mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6df0d04a1e | ||
|
|
aa9c1e7c96 | ||
|
|
66473054f5 | ||
|
|
e8ddbc5c11 | ||
|
|
dfe4620056 | ||
|
|
848224e2c5 | ||
|
|
aee376cc57 | ||
|
|
0d2a81cd39 | ||
|
|
61e99c9489 | ||
|
|
0eb4159737 | ||
|
|
9f0008375f | ||
|
|
0cf1823e70 | ||
|
|
7f39669053 | ||
|
|
7b82d6e985 | ||
|
|
53b0a7aa74 | ||
|
|
fbb09303af | ||
|
|
ff05ac1e41 | ||
|
|
a6f6c1590d | ||
|
|
52c2466b9e | ||
|
|
fb04ab4e76 | ||
|
|
07e2a83ccb | ||
|
|
065d9eb310 | ||
|
|
5da19f3c9a | ||
|
|
88ec3bad71 | ||
|
|
5ab7868533 | ||
|
|
835dda0899 | ||
|
|
ad4709764a | ||
|
|
55a2932973 | ||
|
|
49cb7e5bd7 | ||
|
|
b95182ded7 | ||
|
|
be28672fd4 | ||
|
|
e26c5fc905 | ||
|
|
543f812eb3 | ||
|
|
0c9f9777c5 | ||
|
|
34f334a71d |
@@ -1,4 +0,0 @@
|
||||
# Exclude directories we don't need from Docker context to improve build time
|
||||
node_modules
|
||||
www
|
||||
src
|
||||
15
.env.example
15
.env.example
@@ -1,15 +0,0 @@
|
||||
# Rename file to .env and populate values
|
||||
# to be able to run tests
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
NEXTAUTH_TWITTER_ID=
|
||||
NEXTAUTH_TWITTER_SECRET=
|
||||
NEXTAUTH_TWITTER_USERNAME=
|
||||
NEXTAUTH_TWITTER_PASSWORD=
|
||||
NEXTAUTH_GITHUB_ID=
|
||||
NEXTAUTH_GITHUB_SECRET=
|
||||
NEXTAUTH_GITHUB_USERNAME=
|
||||
NEXTAUTH_GITHUB_PASSWORD=
|
||||
NEXTAUTH_GOOGLE_ID=
|
||||
NEXTAUTH_GOOGLE_SECRET=
|
||||
NEXTAUTH_GOOGLE_USERNAME=
|
||||
NEXTAUTH_GOOGLE_PASSWORD=
|
||||
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/types/ @balazsorban44 @lluia
|
||||
0
FUNDING.yml → .github/FUNDING.yml
vendored
0
FUNDING.yml → .github/FUNDING.yml
vendored
49
.github/ISSUE_TEMPLATE/bug_report.md
vendored
49
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -2,33 +2,42 @@
|
||||
name: Bug report
|
||||
about: Report a defect with NextAuth.js
|
||||
labels: bug
|
||||
assignees: ''
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of the bug in NextAuth.js.
|
||||
## Description 🐜
|
||||
|
||||
Do not report bugs with your own project here, ask from help by raising a question instead - this helps us a lot with administration overhead.
|
||||
Please provide a clear and concise description of the bug in NextAuth.js.
|
||||
|
||||
**Steps to reproduce**
|
||||
Steps to reproduce the behavior.
|
||||
🚧 – _Do not report bugs with your own project here; ask for help [by raising a question instead](https://github.com/nextauthjs/next-auth/issues/new?assignees=&labels=question&template=question.md) - this helps us a lot with administration overhead._
|
||||
|
||||
Include a link to public repository which can be used to reproduce the behaviour.
|
||||
## How to reproduce ☕️
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
We encourage you to use one of the templates set up on **CodeSandbox** to reproduce your issue:
|
||||
|
||||
**Screenshots or error logs**
|
||||
If applicable add screenshots or error logs to help explain the problem.
|
||||
- [`next-auth-example`](https://codesandbox.io/s/next-auth-example-1kktb)
|
||||
- [`next-auth-typescript-example`](https://codesandbox.io/s/next-auth-typescript-example-se32w)
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
🚧 – _If you don't provide any way to reproduce the bug, the issue is at risk of being closed._
|
||||
|
||||
**Feedback**
|
||||
*Documentation refers to searching through [online documentation](https://next-auth.js.org), code comments and issue history. The example project refers to [next-auth-example](https://github.com/iaincollins/next-auth-example).*
|
||||
## Screenshots / Logs 📽
|
||||
|
||||
* [ ] Found the documentation helpful
|
||||
* [ ] Found documentation but was incomplete
|
||||
* [ ] Could not find relevant documentation
|
||||
* [ ] Found the example project helpful
|
||||
* [ ] Did not find the example project helpful
|
||||
**Help us help you**. We can address the bug you found much faster if you provide contextual screenshots or screen recordings showcasing the issue.
|
||||
|
||||
See [Kap](https://getkap.co/) for a good, easy-to-use, cross-platform screen recording tool.
|
||||
|
||||
## Environment 🖥
|
||||
|
||||
Please run this command:
|
||||
|
||||
```
|
||||
$ npx envinfo --system --binaries --browsers --npmPackages "{next-auth}"
|
||||
```
|
||||
|
||||
and paste the output here.
|
||||
|
||||
## Contributing 🙌🏽
|
||||
|
||||
It takes a lot of work 🏋🏻♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
|
||||
|
||||
In case you're willing to help fix this bug, please let us know here, and we'll reach you 😊 . Otherwise, you can have a look at the issues labelled with [`"good first issue"`](https://github.com/nextauthjs/next-auth/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and pick any of them.
|
||||
|
||||
41
.github/ISSUE_TEMPLATE/feature_request.md
vendored
41
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -2,25 +2,38 @@
|
||||
name: Feature request
|
||||
about: Suggest an idea for NextAuth.js
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Summary of proposed feature**
|
||||
A clear and concise description of the feature being proposed.
|
||||
## Summary 💭
|
||||
|
||||
**Purpose of proposed feature**
|
||||
A clear and concise description of why this feature is necessary and what problems it solves.
|
||||
A clear and concise summary of the feature being proposed.
|
||||
|
||||
**Detail about proposed feature**
|
||||
A detailed description of how the proposal might work (if you have one).
|
||||
## Description 📓
|
||||
|
||||
**Potential problems**
|
||||
Describe any potential problems or potential limitations or caveats that might apply to the proposed solution.
|
||||
Please provide a more in-depth description of the feature proposed.
|
||||
|
||||
**Describe any alternatives you've considered**
|
||||
A clear and concise description of any alternative options you've considered.
|
||||
Make sure you provide plenty of [links]() to external documentation and inline code examples like so:
|
||||
|
||||
**Additional context**
|
||||
Any other context, screenshots, etc.
|
||||
```js
|
||||
function myAwesomeNextAuthFeature() {
|
||||
return 💚
|
||||
}
|
||||
```
|
||||
|
||||
*Please indicate if you are willing and able to help implement the proposed feature.*
|
||||
Take time thinking about what you want to say and help us understand your proposal making sure that this description contains:
|
||||
|
||||
- **purpose of the feature**
|
||||
- **potential problems**
|
||||
- **potential alternatives**
|
||||
|
||||
You can use one of the templates set up on **CodeSandbox** to better illustrate your idea:
|
||||
|
||||
- [`next-auth-example`](https://codesandbox.io/s/next-auth-example-1kktb)
|
||||
- [`next-auth-typescript-example`](https://codesandbox.io/s/next-auth-typescript-example-se32w)
|
||||
|
||||
## Contributing 🙌🏽
|
||||
|
||||
It takes a lot of work 🏋🏻♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
|
||||
|
||||
In case you're willing to help implement this feature, please let us know here, and we'll reach you 😊 . Otherwise, you can have a look at the issues labelled with [`"good first issue"`](https://github.com/nextauthjs/next-auth/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and pick any of them.
|
||||
|
||||
37
.github/ISSUE_TEMPLATE/question.md
vendored
37
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -2,24 +2,31 @@
|
||||
name: Question
|
||||
about: Ask a question about NextAuth.js or for help using it
|
||||
labels: question
|
||||
assignees: ''
|
||||
assignees: ""
|
||||
---
|
||||
<!-- NOTE: Questions will be converted to Discussions. You can find them at https://github.com/nextauthjs/next-auth/discussions! -->
|
||||
|
||||
**Your question**
|
||||
<!-- A clear and concise question. -->
|
||||
## Question 💬
|
||||
|
||||
**What are you trying to do**
|
||||
<!-- A description of what you are trying to do, for context. -->
|
||||
Please provide an in-depth description of the question you have.
|
||||
|
||||
**Reproduction**
|
||||
<!-- If your question is code related, adding a reproduction to your use case can greatly reduce the time it takes us to figure out how to better help you. -->
|
||||
Make sure you [link]() to external documentation if necessary and provide inline code examples like so:
|
||||
|
||||
**Feedback**
|
||||
*Documentation refers to searching through [online documentation](https://next-auth.js.org), code comments and issue history. The example project refers to [next-auth-example](https://github.com/iaincollins/next-auth-example).*
|
||||
```js
|
||||
function myAwesomeNextAuthFeature() {
|
||||
return 💚
|
||||
}
|
||||
```
|
||||
|
||||
* [ ] Found the documentation helpful
|
||||
* [ ] Found documentation but was incomplete
|
||||
* [ ] Could not find relevant documentation
|
||||
* [ ] Found the example project helpful
|
||||
* [ ] Did not find the example project helpful
|
||||
**NOTE:** Questions will be converted to Discussions. You can find them [here](https://github.com/nextauthjs/next-auth/discussions)!
|
||||
|
||||
## How to reproduce ☕️
|
||||
|
||||
We encourage you to use the template set-up on **CodeSandbox** as a playground to represent your question or doubt:
|
||||
|
||||
- [`next-auth-example`](https://codesandbox.io/s/next-auth-example-1kktb)
|
||||
|
||||
## Contributing 🙌🏽
|
||||
|
||||
It takes a lot of work 🏋🏻♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
|
||||
|
||||
In case you're willing to help answer this question, please let us know here, and we'll reach you 😊 . Otherwise, you can have a look at the issues labelled with [`"good first issue"`](https://github.com/nextauthjs/next-auth/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and pick any of them.
|
||||
|
||||
36
.github/ISSUE_TEMPLATE/typescript.md
vendored
Normal file
36
.github/ISSUE_TEMPLATE/typescript.md
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: TypeScript
|
||||
about: Ask a question about NextAuth.js TypeScript integration
|
||||
labels:
|
||||
- question
|
||||
- TypeScript
|
||||
assignees:
|
||||
- lluia
|
||||
- balazsorban44
|
||||
---
|
||||
|
||||
## Question 💬
|
||||
|
||||
Please provide an in-depth description of the question you have when using NextAuth.js on a Typescript project or when consuming the built-in types for `next-auth`.
|
||||
|
||||
Make sure you [link]() to external documentation if necessary and provide inline code examples like so:
|
||||
|
||||
```js
|
||||
function myAwesomeNextAuthFeature() {
|
||||
return 💚
|
||||
}
|
||||
```
|
||||
|
||||
**NOTE:** Questions will be converted to Discussions. You can find them [here](https://github.com/nextauthjs/next-auth/discussions)!
|
||||
|
||||
## How to reproduce ☕️
|
||||
|
||||
We encourage you to use the template set-up on **CodeSandbox** as a playground to represent your question or doubt:
|
||||
|
||||
- [`next-auth-typescript-example`](https://codesandbox.io/s/next-auth-typescript-example-se32w)
|
||||
|
||||
## Contributing 🙌🏽
|
||||
|
||||
It takes a lot of work 🏋🏻♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
|
||||
|
||||
In case you're willing to help answer this TypeScript question, please let us know here, and we'll reach you 😊 . Otherwise, you can have a look at the issues labelled with [`"good first issue"`](https://github.com/nextauthjs/next-auth/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and pick any of them.
|
||||
29
.github/PULL_REQUEST_TEMPLATE.md
vendored
29
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -16,26 +16,29 @@ merge of your pull request!
|
||||
|
||||
<!-- What changes are being made? (What feature/bug is being fixed here?) -->
|
||||
|
||||
**What**:
|
||||
## Reasoning 💡
|
||||
|
||||
<!-- Why are these changes necessary? -->
|
||||
What changes are being made? What feature/bug is being fixed here?
|
||||
|
||||
**Why**:
|
||||
## Checklist 🧢
|
||||
|
||||
<!-- How were these changes implemented? -->
|
||||
Feel free cross items ( like this `~[] item~` ) if they're irrelevant to your changes.
|
||||
|
||||
**How**:
|
||||
|
||||
<!-- Have you done all of these things? -->
|
||||
|
||||
**Checklist**:
|
||||
|
||||
<!-- add "N/A" to the end of each line that's irrelevant to your changes -->
|
||||
<!-- to check an item, place an "x" in the box like so: "- [x] Documentation" -->
|
||||
To check an item, place an `x` in the box like so: `- [x] Documentation`.
|
||||
|
||||
- [ ] Documentation
|
||||
- [ ] Tests
|
||||
- [ ] Ready to be merged
|
||||
<!-- In your opinion, is this ready to be merged as soon as it's reviewed? -->
|
||||
|
||||
<!-- feel free to add additional comments -->
|
||||
## Affected issues 🎟
|
||||
|
||||
Please [scout and link issues](https://github.com/nextauthjs/next-auth/issues) that might be solved by this PR.
|
||||
|
||||
If you write `"Fixes"` or `"Closes"` before the issue link like so:
|
||||
|
||||
```
|
||||
Fixes #359
|
||||
```
|
||||
|
||||
the connected issue will be automatically closed once the PR is merged and hence help with maintenance of the library 😊
|
||||
|
||||
18
.github/workflows/build.yml
vendored
18
.github/workflows/build.yml
vendored
@@ -4,22 +4,22 @@ name: Lint/Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- next
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- next
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- next
|
||||
- main
|
||||
- beta
|
||||
- next
|
||||
|
||||
jobs:
|
||||
lint-and-build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [10, 12, 14]
|
||||
node-version: [12, 14, 16]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
@@ -29,4 +29,4 @@ jobs:
|
||||
- name: Install dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
- run: npm run lint
|
||||
- run: npm run build
|
||||
- run: npm run build
|
||||
|
||||
2
.github/workflows/integration.yml
vendored
2
.github/workflows/integration.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [10, 12, 14]
|
||||
node-version: [12, 14, 16]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
2
.github/workflows/types.yml
vendored
2
.github/workflows/types.yml
vendored
@@ -19,6 +19,8 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14
|
||||
- name: Install dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
- name: Check types
|
||||
|
||||
25
.gitignore
vendored
25
.gitignore
vendored
@@ -25,20 +25,21 @@ node_modules
|
||||
# Generated files
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
.next
|
||||
www/providers.json
|
||||
src/providers/index.js
|
||||
internals
|
||||
adapters.d.ts
|
||||
adapters.js
|
||||
client.d.ts
|
||||
client.js
|
||||
index.d.ts
|
||||
index.js
|
||||
jwt.d.ts
|
||||
jwt.js
|
||||
providers.d.ts
|
||||
providers.js
|
||||
/internals
|
||||
/adapters.d.ts
|
||||
/adapters.js
|
||||
/client.d.ts
|
||||
/client.js
|
||||
/index.d.ts
|
||||
/index.js
|
||||
/jwt.d.ts
|
||||
/jwt.js
|
||||
/providers.d.ts
|
||||
/providers.js
|
||||
/errors.js
|
||||
/errors.d.ts
|
||||
|
||||
# Development app
|
||||
app/next-auth
|
||||
|
||||
30
Dockerfile
30
Dockerfile
@@ -1,30 +0,0 @@
|
||||
# Multi stage build to allow us to improve performance
|
||||
FROM node:10-alpine as base
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Install basic dependancies (Next.js, React)
|
||||
COPY test/docker/app/package*.json ./
|
||||
RUN npm ci --only=production
|
||||
|
||||
FROM node:10-alpine as app
|
||||
COPY --from=base /usr/src/app ./
|
||||
|
||||
# Copy last build of library into the image and install dependences for it.
|
||||
# This ensures the build is valid and package.json contains everything needed
|
||||
# to actually run the library.
|
||||
# Note: You must run `npm run build` first to build a release of the library
|
||||
RUN mkdir -p node_modules/next-auth
|
||||
# Copy all entrypoints for the library (if creating a new one, add it here)
|
||||
COPY index.js providers.js adapters.js client.js jwt.js node_modules/next-auth/
|
||||
# Copy the dist dir
|
||||
COPY dist node_modules/next-auth/dist
|
||||
# Copy the package.json for the library and install it's dependences
|
||||
COPY package*.json node_modules/next-auth/
|
||||
RUN cd node_modules/next-auth/ && npm ci --only=production
|
||||
|
||||
# Copy test pages across
|
||||
COPY test/docker/app/pages ./pages
|
||||
|
||||
RUN npm run build
|
||||
|
||||
CMD [ "npm", "start" ]
|
||||
@@ -82,6 +82,6 @@ export default NextAuth({
|
||||
// Prisma Database Adapter
|
||||
// To configure this app to use the schema in `prisma/schema.prisma` run:
|
||||
// npx prisma generate
|
||||
// npx prisma migrate dev --preview-feature
|
||||
// npx prisma migrate dev
|
||||
// adapter: Adapters.Prisma.Adapter({ prisma })
|
||||
})
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
"presets": [
|
||||
["@babel/preset-env", { "targets": { "esmodules": true } }]
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-class-properties"
|
||||
],
|
||||
"comments": false,
|
||||
"overrides": [
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@ const MODULE_ENTRIES = {
|
||||
PROVIDERS: "providers",
|
||||
ADAPTERS: "adapters",
|
||||
JWT: "jwt",
|
||||
ERRORS: "errors",
|
||||
}
|
||||
|
||||
// Building submodule entries
|
||||
@@ -17,6 +18,7 @@ const BUILD_TARGETS = {
|
||||
[`${MODULE_ENTRIES.ADAPTERS}.js`]: "module.exports = require('./dist/adapters').default\n",
|
||||
[`${MODULE_ENTRIES.PROVIDERS}.js`]: "module.exports = require('./dist/providers').default\n",
|
||||
[`${MODULE_ENTRIES.JWT}.js`]: "module.exports = require('./dist/lib/jwt').default\n",
|
||||
[`${MODULE_ENTRIES.ERRORS}.js`]: "module.exports = require('./dist/lib/errors').default\n",
|
||||
}
|
||||
|
||||
Object.entries(BUILD_TARGETS).forEach(([target, content]) => {
|
||||
@@ -34,6 +36,7 @@ const TYPES_TARGETS = [
|
||||
`${MODULE_ENTRIES.ADAPTERS}.d.ts`,
|
||||
`${MODULE_ENTRIES.PROVIDERS}.d.ts`,
|
||||
`${MODULE_ENTRIES.JWT}.d.ts`,
|
||||
`${MODULE_ENTRIES.ERRORS}.d.ts`,
|
||||
"internals",
|
||||
]
|
||||
|
||||
|
||||
195
package-lock.json
generated
195
package-lock.json
generated
@@ -991,19 +991,183 @@
|
||||
}
|
||||
},
|
||||
"@babel/plugin-proposal-class-properties": {
|
||||
"version": "7.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.12.1.tgz",
|
||||
"integrity": "sha512-cKp3dlQsFsEs5CWKnN7BnSHOd0EOW8EKpEjkoz1pO2E5KzIDNV9Ros1b0CnmbVgAGXJubOYVBOGCT1OmJwOI7w==",
|
||||
"version": "7.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.13.0.tgz",
|
||||
"integrity": "sha512-KnTDjFNC1g+45ka0myZNvSBFLhNCLN+GeGYLDEA8Oq7MZ6yMgfLoIRh86GRT0FjtJhZw8JyUskP9uvj5pHM9Zg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-create-class-features-plugin": "^7.12.1",
|
||||
"@babel/helper-plugin-utils": "^7.10.4"
|
||||
"@babel/helper-create-class-features-plugin": "^7.13.0",
|
||||
"@babel/helper-plugin-utils": "^7.13.0"
|
||||
},
|
||||
"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/generator": {
|
||||
"version": "7.13.16",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.13.16.tgz",
|
||||
"integrity": "sha512-grBBR75UnKOcUWMp8WoDxNsWCFl//XCK6HWTrBQKTr5SV9f5g0pNOjdyzi/DTBv12S9GnYPInIXQBTky7OXEMg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.13.16",
|
||||
"jsesc": "^2.5.1",
|
||||
"source-map": "^0.5.0"
|
||||
}
|
||||
},
|
||||
"@babel/helper-create-class-features-plugin": {
|
||||
"version": "7.13.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.13.11.tgz",
|
||||
"integrity": "sha512-ays0I7XYq9xbjCSvT+EvysLgfc3tOkwCULHjrnscGT3A9qD4sk3wXnJ3of0MAWsWGjdinFvajHU2smYuqXKMrw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-function-name": "^7.12.13",
|
||||
"@babel/helper-member-expression-to-functions": "^7.13.0",
|
||||
"@babel/helper-optimise-call-expression": "^7.12.13",
|
||||
"@babel/helper-replace-supers": "^7.13.0",
|
||||
"@babel/helper-split-export-declaration": "^7.12.13"
|
||||
}
|
||||
},
|
||||
"@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-member-expression-to-functions": {
|
||||
"version": "7.13.12",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.12.tgz",
|
||||
"integrity": "sha512-48ql1CLL59aKbU94Y88Xgb2VFy7a95ykGRbJJaaVv+LX5U8wFpLfiGXJJGUozsmA1oEh/o5Bp60Voq7ACyA/Sw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.13.12"
|
||||
}
|
||||
},
|
||||
"@babel/helper-optimise-call-expression": {
|
||||
"version": "7.12.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz",
|
||||
"integrity": "sha512-BdWQhoVJkp6nVjB7nkFWcn43dkprYauqtk++Py2eaf/GRDFm5BxRqEIZCiHlZUGAVmtwKcsVL1dC68WmzeFmiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.12.13"
|
||||
}
|
||||
},
|
||||
"@babel/helper-plugin-utils": {
|
||||
"version": "7.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz",
|
||||
"integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==",
|
||||
"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-replace-supers": {
|
||||
"version": "7.13.12",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.13.12.tgz",
|
||||
"integrity": "sha512-Gz1eiX+4yDO8mT+heB94aLVNCL+rbuT2xy4YfyNqu8F+OI6vMvJK891qGBTqL9Uc8wxEvRW92Id6G7sDen3fFw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-member-expression-to-functions": "^7.13.12",
|
||||
"@babel/helper-optimise-call-expression": "^7.12.13",
|
||||
"@babel/traverse": "^7.13.0",
|
||||
"@babel/types": "^7.13.12"
|
||||
}
|
||||
},
|
||||
"@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.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz",
|
||||
"integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/highlight": {
|
||||
"version": "7.13.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.13.10.tgz",
|
||||
"integrity": "sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"chalk": "^2.0.0",
|
||||
"js-tokens": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.13.16",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.16.tgz",
|
||||
"integrity": "sha512-6bAg36mCwuqLO0hbR+z7PHuqWiCeP7Dzg73OpQwsAB1Eb8HnGEz5xYBzCfbu+YjoaJsJs+qheDxVAuqbt3ILEw==",
|
||||
"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.13.17",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.13.17.tgz",
|
||||
"integrity": "sha512-BMnZn0R+X6ayqm3C3To7o1j7Q020gWdqdyP50KEoVqaCO2c/Im7sYZSmVgvefp8TTMQ+9CtwuBp0Z1CZ8V3Pvg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.12.13",
|
||||
"@babel/generator": "^7.13.16",
|
||||
"@babel/helper-function-name": "^7.12.13",
|
||||
"@babel/helper-split-export-declaration": "^7.12.13",
|
||||
"@babel/parser": "^7.13.16",
|
||||
"@babel/types": "^7.13.17",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.13.17",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.17.tgz",
|
||||
"integrity": "sha512-RawydLgxbOPDlTLJNtoIypwdmAy//uQIzlKt2+iBiJaRlVuI6QLUxVAyWGNfOzp8Yu4L4lLIacoCyTNtpb4wiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"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
|
||||
}
|
||||
}
|
||||
@@ -2344,6 +2508,21 @@
|
||||
"integrity": "sha512-CAEbWH7OIur6jEOzaai83jq3FmKmv4PmX1JYfs9IrYcGEVI/lyL1EXJGCj7eFVJ0bg5QR8LMxBlEtA+xKiLpFw==",
|
||||
"dev": true
|
||||
},
|
||||
"@next-auth/prisma-legacy-adapter": {
|
||||
"version": "0.0.1-canary.115",
|
||||
"resolved": "https://registry.npmjs.org/@next-auth/prisma-legacy-adapter/-/prisma-legacy-adapter-0.0.1-canary.115.tgz",
|
||||
"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==",
|
||||
"requires": {
|
||||
"crypto-js": "^4.0.0",
|
||||
"require_optional": "^1.0.1",
|
||||
"typeorm": "^0.2.30"
|
||||
}
|
||||
},
|
||||
"@next/env": {
|
||||
"version": "10.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-10.0.5.tgz",
|
||||
|
||||
58
package.json
58
package.json
@@ -7,14 +7,26 @@
|
||||
"author": "Iain Collins <me@iaincollins.com>",
|
||||
"main": "index.js",
|
||||
"types": "./index.d.ts",
|
||||
"keywords": ["react", "nodejs", "oauth", "jwt", "oauth2", "authentication", "nextjs", "csrf", "oidc", "nextauth"],
|
||||
"keywords": [
|
||||
"react",
|
||||
"nodejs",
|
||||
"oauth",
|
||||
"jwt",
|
||||
"oauth2",
|
||||
"authentication",
|
||||
"nextjs",
|
||||
"csrf",
|
||||
"oidc",
|
||||
"nextauth"
|
||||
],
|
||||
"exports": {
|
||||
".": "./dist/server/index.js",
|
||||
"./jwt": "./dist/lib/jwt.js",
|
||||
"./adapters": "./dist/adapters/index.js",
|
||||
"./client": "./dist/client/index.js",
|
||||
"./providers": "./dist/providers/index.js",
|
||||
"./providers/*": "./dist/providers/*.js"
|
||||
"./providers/*": "./dist/providers/*.js",
|
||||
"./errors": "./dist/lib/errors.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run build:js && npm run build:css",
|
||||
@@ -25,22 +37,9 @@
|
||||
"watch": "npm run watch:js | npm run watch:css",
|
||||
"watch:js": "babel --config-file ./config/babel.config.json --watch src --out-dir dist",
|
||||
"watch:css": "postcss --config config/postcss.config.js --watch src/**/*.css --base src --dir dist",
|
||||
"test:app:start": "docker-compose -f test/docker/app.yml up -d",
|
||||
"test:app:rebuild": "npm run build && docker-compose -f test/docker/app.yml up -d --build",
|
||||
"test:app:stop": "docker-compose -f test/docker/app.yml down",
|
||||
"test": "npm run test:app:rebuild && npm run test:integration && npm run test:app:stop && npm run test:types",
|
||||
"test:db": "npm run test:db:mysql && npm run test:db:postgres && npm run test:db:mongodb && npm run test:db:mssql",
|
||||
"test:db:mysql": "node test/mysql.js",
|
||||
"test:db:postgres": "node test/postgres.js",
|
||||
"test:db:mongodb": "node test/mongodb.js",
|
||||
"test:db:mssql": "node test/mssql.js",
|
||||
"test:integration": "mocha test/integration",
|
||||
"test": "echo \"Write some tests...\"; npm run test:types",
|
||||
"test:types": "dtslint types",
|
||||
"db:start": "docker-compose -f test/docker/databases.yml up -d",
|
||||
"db:stop": "docker-compose -f test/docker/databases.yml down",
|
||||
"prepublishOnly": "npm run build",
|
||||
"publish:beta": "npm publish --tag beta",
|
||||
"publish:canary": "npm publish --tag canary",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix"
|
||||
},
|
||||
@@ -54,12 +53,16 @@
|
||||
"adapters.d.ts",
|
||||
"client.js",
|
||||
"client.d.ts",
|
||||
"errors.js",
|
||||
"errors.d.ts",
|
||||
"jwt.js",
|
||||
"jwt.d.ts",
|
||||
"internals"
|
||||
],
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@next-auth/prisma-legacy-adapter": "canary",
|
||||
"@next-auth/typeorm-legacy-adapter": "canary",
|
||||
"crypto-js": "^4.0.0",
|
||||
"futoin-hkdf": "^1.3.2",
|
||||
"jose": "^1.27.2",
|
||||
@@ -87,6 +90,7 @@
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.8.4",
|
||||
"@babel/core": "^7.9.6",
|
||||
"@babel/plugin-proposal-class-properties": "^7.13.0",
|
||||
"@babel/preset-env": "^7.9.6",
|
||||
"@prisma/client": "^2.16.1",
|
||||
"@semantic-release/commit-analyzer": "^8.0.1",
|
||||
@@ -157,8 +161,26 @@
|
||||
"branches": [
|
||||
"+([0-9])?(.{+([0-9]),x}).x",
|
||||
"main",
|
||||
{ "name": "beta", "prerelease": true },
|
||||
{ "name": "next", "prerelease": true }
|
||||
{
|
||||
"name": "beta",
|
||||
"prerelease": true
|
||||
},
|
||||
{
|
||||
"name": "next",
|
||||
"prerelease": true
|
||||
}
|
||||
],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
"@semantic-release/release-notes-generator",
|
||||
"@semantic-release/npm",
|
||||
[
|
||||
"@semantic-release/github",
|
||||
{
|
||||
"releasedLabels": false,
|
||||
"successComment": false
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"funding": [
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
const Adapter = (config, options = {}) => {
|
||||
async function getAdapter (appOptions) {
|
||||
const { logger } = appOptions
|
||||
// Display debug output if debug option enabled
|
||||
function debug (debugCode, ...args) {
|
||||
logger.debug(`ADAPTER_${debugCode}`, ...args)
|
||||
}
|
||||
|
||||
async function createUser (profile) {
|
||||
debug('createUser', profile)
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUser (id) {
|
||||
debug('getUser', id)
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUserByEmail (email) {
|
||||
debug('getUserByEmail', email)
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUserByProviderAccountId (providerId, providerAccountId) {
|
||||
debug('getUserByProviderAccountId', providerId, providerAccountId)
|
||||
return null
|
||||
}
|
||||
|
||||
async function updateUser (user) {
|
||||
debug('updateUser', user)
|
||||
return null
|
||||
}
|
||||
|
||||
async function deleteUser (userId) {
|
||||
debug('deleteUser', userId)
|
||||
return null
|
||||
}
|
||||
|
||||
async function linkAccount (userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires) {
|
||||
debug('linkAccount', userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires)
|
||||
return null
|
||||
}
|
||||
|
||||
async function unlinkAccount (userId, providerId, providerAccountId) {
|
||||
debug('unlinkAccount', userId, providerId, providerAccountId)
|
||||
return null
|
||||
}
|
||||
|
||||
async function createSession (user) {
|
||||
debug('createSession', user)
|
||||
return null
|
||||
}
|
||||
|
||||
async function getSession (sessionToken) {
|
||||
debug('getSession', sessionToken)
|
||||
return null
|
||||
}
|
||||
|
||||
async function updateSession (session, force) {
|
||||
debug('updateSession', session)
|
||||
return null
|
||||
}
|
||||
|
||||
async function deleteSession (sessionToken) {
|
||||
debug('deleteSession', sessionToken)
|
||||
return null
|
||||
}
|
||||
|
||||
async function createVerificationRequest (identifier, url, token, secret, provider) {
|
||||
debug('createVerificationRequest', identifier)
|
||||
return null
|
||||
}
|
||||
|
||||
async function getVerificationRequest (identifier, token, secret, provider) {
|
||||
debug('getVerificationRequest', identifier, token)
|
||||
return null
|
||||
}
|
||||
|
||||
async function deleteVerificationRequest (identifier, token, secret, provider) {
|
||||
debug('deleteVerification', identifier, token)
|
||||
return null
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
createUser,
|
||||
getUser,
|
||||
getUserByEmail,
|
||||
getUserByProviderAccountId,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
linkAccount,
|
||||
unlinkAccount,
|
||||
createSession,
|
||||
getSession,
|
||||
updateSession,
|
||||
deleteSession,
|
||||
createVerificationRequest,
|
||||
getVerificationRequest,
|
||||
deleteVerificationRequest
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
getAdapter
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
Adapter
|
||||
}
|
||||
8
src/adapters/prisma.js
Normal file
8
src/adapters/prisma.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
* Source code is now at:
|
||||
* https://github.com/nextauthjs/adapters/tree/canary/packages/prisma-legacy
|
||||
*/
|
||||
|
||||
import PrismaLegacyAdapter from "@next-auth/prisma-legacy-adapter"
|
||||
|
||||
export default PrismaLegacyAdapter
|
||||
@@ -1,340 +0,0 @@
|
||||
import { createHash, randomBytes } from 'crypto'
|
||||
|
||||
import { CreateUserError } from '../../lib/errors'
|
||||
|
||||
const Adapter = (config) => {
|
||||
const {
|
||||
prisma,
|
||||
modelMapping = {
|
||||
User: 'user',
|
||||
Account: 'account',
|
||||
Session: 'session',
|
||||
VerificationRequest: 'verificationRequest'
|
||||
}
|
||||
} = config
|
||||
|
||||
const { User, Account, Session, VerificationRequest } = modelMapping
|
||||
|
||||
function getCompoundId (providerId, providerAccountId) {
|
||||
return createHash('sha256').update(`${providerId}:${providerAccountId}`).digest('hex')
|
||||
}
|
||||
|
||||
async function getAdapter (appOptions) {
|
||||
const { logger } = appOptions
|
||||
function debug (debugCode, ...args) {
|
||||
logger.debug(`PRISMA_${debugCode}`, ...args)
|
||||
}
|
||||
|
||||
if (appOptions && (!appOptions.session || !appOptions.session.maxAge)) {
|
||||
debug('GET_ADAPTER', 'Session expiry not configured (defaulting to 30 days')
|
||||
}
|
||||
|
||||
const defaultSessionMaxAge = 30 * 24 * 60 * 60 * 1000
|
||||
const sessionMaxAge = (appOptions && appOptions.session && appOptions.session.maxAge)
|
||||
? appOptions.session.maxAge * 1000
|
||||
: defaultSessionMaxAge
|
||||
const sessionUpdateAge = (appOptions && appOptions.session && appOptions.session.updateAge)
|
||||
? appOptions.session.updateAge * 1000
|
||||
: 0
|
||||
|
||||
async function createUser (profile) {
|
||||
debug('CREATE_USER', profile)
|
||||
try {
|
||||
return prisma[User].create({
|
||||
data: {
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: profile.image,
|
||||
emailVerified: profile.emailVerified ? profile.emailVerified.toISOString() : null
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('CREATE_USER_ERROR', error)
|
||||
return Promise.reject(new CreateUserError(error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getUser (id) {
|
||||
debug('GET_USER', id)
|
||||
try {
|
||||
return prisma[User].findUnique({ where: { id } })
|
||||
} catch (error) {
|
||||
logger.error('GET_USER_BY_ID_ERROR', error)
|
||||
return Promise.reject(new Error('GET_USER_BY_ID_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getUserByEmail (email) {
|
||||
debug('GET_USER_BY_EMAIL', email)
|
||||
try {
|
||||
if (!email) { return Promise.resolve(null) }
|
||||
return prisma[User].findUnique({ where: { email } })
|
||||
} catch (error) {
|
||||
logger.error('GET_USER_BY_EMAIL_ERROR', error)
|
||||
return Promise.reject(new Error('GET_USER_BY_EMAIL_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getUserByProviderAccountId (providerId, providerAccountId) {
|
||||
debug('GET_USER_BY_PROVIDER_ACCOUNT_ID', providerId, providerAccountId)
|
||||
try {
|
||||
const account = await prisma[Account].findUnique({ where: { compoundId: getCompoundId(providerId, providerAccountId) } })
|
||||
if (!account) { return null }
|
||||
return prisma[User].findUnique({ where: { id: account.userId } })
|
||||
} catch (error) {
|
||||
logger.error('GET_USER_BY_PROVIDER_ACCOUNT_ID_ERROR', error)
|
||||
return Promise.reject(new Error('GET_USER_BY_PROVIDER_ACCOUNT_ID_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function updateUser (user) {
|
||||
debug('UPDATE_USER', user)
|
||||
try {
|
||||
const { id, name, email, image, emailVerified } = user
|
||||
return prisma[User].update({
|
||||
where: { id },
|
||||
data: {
|
||||
name,
|
||||
email,
|
||||
image,
|
||||
emailVerified: emailVerified ? emailVerified.toISOString() : null
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('UPDATE_USER_ERROR', error)
|
||||
return Promise.reject(new Error('UPDATE_USER_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteUser (userId) {
|
||||
debug('DELETE_USER', userId)
|
||||
try {
|
||||
return prisma[User].delete({ where: { id: userId } })
|
||||
} catch (error) {
|
||||
logger.error('DELETE_USER_ERROR', error)
|
||||
return Promise.reject(new Error('DELETE_USER_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function linkAccount (userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires) {
|
||||
debug('LINK_ACCOUNT', userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires)
|
||||
try {
|
||||
return prisma[Account].create({
|
||||
data: {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
compoundId: getCompoundId(providerId, providerAccountId),
|
||||
providerAccountId: `${providerAccountId}`,
|
||||
providerId,
|
||||
providerType,
|
||||
accessTokenExpires,
|
||||
userId
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('LINK_ACCOUNT_ERROR', error)
|
||||
return Promise.reject(new Error('LINK_ACCOUNT_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function unlinkAccount (userId, providerId, providerAccountId) {
|
||||
debug('UNLINK_ACCOUNT', userId, providerId, providerAccountId)
|
||||
try {
|
||||
return prisma[Account].delete({ where: { compoundId: getCompoundId(providerId, providerAccountId) } })
|
||||
} catch (error) {
|
||||
logger.error('UNLINK_ACCOUNT_ERROR', error)
|
||||
return Promise.reject(new Error('UNLINK_ACCOUNT_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function createSession (user) {
|
||||
debug('CREATE_SESSION', user)
|
||||
try {
|
||||
let expires = null
|
||||
if (sessionMaxAge) {
|
||||
const dateExpires = new Date()
|
||||
dateExpires.setTime(dateExpires.getTime() + sessionMaxAge)
|
||||
expires = dateExpires.toISOString()
|
||||
}
|
||||
|
||||
return prisma[Session].create({
|
||||
data: {
|
||||
expires,
|
||||
userId: user.id,
|
||||
sessionToken: randomBytes(32).toString('hex'),
|
||||
accessToken: randomBytes(32).toString('hex')
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('CREATE_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('CREATE_SESSION_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getSession (sessionToken) {
|
||||
debug('GET_SESSION', sessionToken)
|
||||
try {
|
||||
const session = await prisma[Session].findUnique({ where: { sessionToken } })
|
||||
|
||||
// Check session has not expired (do not return it if it has)
|
||||
if (session && session.expires && new Date() > session.expires) {
|
||||
await prisma[Session].delete({ where: { sessionToken } })
|
||||
return null
|
||||
}
|
||||
|
||||
return session
|
||||
} catch (error) {
|
||||
logger.error('GET_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('GET_SESSION_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSession (session, force) {
|
||||
debug('UPDATE_SESSION', session)
|
||||
try {
|
||||
if (sessionMaxAge && (sessionUpdateAge || sessionUpdateAge === 0) && session.expires) {
|
||||
// Calculate last updated date, to throttle write updates to database
|
||||
// Formula: ({expiry date} - sessionMaxAge) + sessionUpdateAge
|
||||
// e.g. ({expiry date} - 30 days) + 1 hour
|
||||
//
|
||||
// Default for sessionMaxAge is 30 days.
|
||||
// Default for sessionUpdateAge is 1 hour.
|
||||
const dateSessionIsDueToBeUpdated = new Date(session.expires)
|
||||
dateSessionIsDueToBeUpdated.setTime(dateSessionIsDueToBeUpdated.getTime() - sessionMaxAge)
|
||||
dateSessionIsDueToBeUpdated.setTime(dateSessionIsDueToBeUpdated.getTime() + sessionUpdateAge)
|
||||
|
||||
// Trigger update of session expiry date and write to database, only
|
||||
// if the session was last updated more than {sessionUpdateAge} ago
|
||||
if (new Date() > dateSessionIsDueToBeUpdated) {
|
||||
const newExpiryDate = new Date()
|
||||
newExpiryDate.setTime(newExpiryDate.getTime() + sessionMaxAge)
|
||||
session.expires = newExpiryDate
|
||||
} else if (!force) {
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
// If session MaxAge, session UpdateAge or session.expires are
|
||||
// missing then don't even try to save changes, unless force is set.
|
||||
if (!force) { return null }
|
||||
}
|
||||
|
||||
const { id, expires } = session
|
||||
return prisma[Session].update({ where: { id }, data: { expires: expires.toISOString() } })
|
||||
} catch (error) {
|
||||
logger.error('UPDATE_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('UPDATE_SESSION_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSession (sessionToken) {
|
||||
debug('DELETE_SESSION', sessionToken)
|
||||
try {
|
||||
return prisma[Session].delete({ where: { sessionToken } })
|
||||
} catch (error) {
|
||||
logger.error('DELETE_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('DELETE_SESSION_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function createVerificationRequest (identifier, url, token, secret, provider) {
|
||||
debug('CREATE_VERIFICATION_REQUEST', identifier)
|
||||
try {
|
||||
const { baseUrl } = appOptions
|
||||
const { sendVerificationRequest, maxAge } = provider
|
||||
|
||||
// Store hashed token (using secret as salt) so that tokens cannot be exploited
|
||||
// even if the contents of the database is compromised.
|
||||
// @TODO Use bcrypt function here instead of simple salted hash
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
|
||||
let expires = null
|
||||
if (maxAge) {
|
||||
const dateExpires = new Date()
|
||||
dateExpires.setTime(dateExpires.getTime() + (maxAge * 1000))
|
||||
expires = dateExpires.toISOString()
|
||||
}
|
||||
|
||||
// Save to database
|
||||
const verificationRequest = await prisma[VerificationRequest].create({
|
||||
data: {
|
||||
identifier,
|
||||
token: hashedToken,
|
||||
expires
|
||||
}
|
||||
})
|
||||
|
||||
// With the verificationCallback on a provider, you can send an email, or queue
|
||||
// an email to be sent, or perform some other action (e.g. send a text message)
|
||||
await sendVerificationRequest({ identifier, url, token, baseUrl, provider })
|
||||
|
||||
return verificationRequest
|
||||
} catch (error) {
|
||||
logger.error('CREATE_VERIFICATION_REQUEST_ERROR', error)
|
||||
return Promise.reject(new Error('CREATE_VERIFICATION_REQUEST_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getVerificationRequest (identifier, token, secret, provider) {
|
||||
debug('GET_VERIFICATION_REQUEST', identifier, token)
|
||||
try {
|
||||
// Hash token provided with secret before trying to match it with database
|
||||
// @TODO Use bcrypt instead of salted SHA-256 hash for token
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
const verificationRequest = await prisma[VerificationRequest].findFirst({
|
||||
where: {
|
||||
identifier,
|
||||
token: hashedToken
|
||||
}
|
||||
})
|
||||
if (verificationRequest && verificationRequest.expires && new Date() > verificationRequest.expires) {
|
||||
// Delete verification entry so it cannot be used again
|
||||
await prisma[VerificationRequest].deleteMany({ where: { identifier, token: hashedToken } })
|
||||
return null
|
||||
}
|
||||
|
||||
return verificationRequest
|
||||
} catch (error) {
|
||||
logger.error('GET_VERIFICATION_REQUEST_ERROR', error)
|
||||
return Promise.reject(new Error('GET_VERIFICATION_REQUEST_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteVerificationRequest (identifier, token, secret, provider) {
|
||||
debug('DELETE_VERIFICATION', identifier, token)
|
||||
try {
|
||||
// Delete verification entry so it cannot be used again
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
await prisma[VerificationRequest].deleteMany({ where: { identifier, token: hashedToken } })
|
||||
} catch (error) {
|
||||
logger.error('DELETE_VERIFICATION_REQUEST_ERROR', error)
|
||||
return Promise.reject(new Error('DELETE_VERIFICATION_REQUEST_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
createUser,
|
||||
getUser,
|
||||
getUserByEmail,
|
||||
getUserByProviderAccountId,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
linkAccount,
|
||||
unlinkAccount,
|
||||
createSession,
|
||||
getSession,
|
||||
updateSession,
|
||||
deleteSession,
|
||||
createVerificationRequest,
|
||||
getVerificationRequest,
|
||||
deleteVerificationRequest
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
getAdapter
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
Adapter
|
||||
}
|
||||
8
src/adapters/typeorm.js
Normal file
8
src/adapters/typeorm.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
* Source code is now at:
|
||||
* https://github.com/nextauthjs/adapters/tree/canary/packages/typeorm-legacy
|
||||
*/
|
||||
|
||||
import TypeORMLegacyAdapter from "@next-auth/typeorm-legacy-adapter"
|
||||
|
||||
export default TypeORMLegacyAdapter
|
||||
@@ -1,384 +0,0 @@
|
||||
import { createConnection, getConnection } from 'typeorm'
|
||||
import { createHash } from 'crypto'
|
||||
import require_optional from 'require_optional' // eslint-disable-line camelcase
|
||||
|
||||
import { CreateUserError } from '../../lib/errors'
|
||||
import adapterConfig from './lib/config'
|
||||
import adapterTransform from './lib/transform'
|
||||
import Models from './models'
|
||||
|
||||
import { updateConnectionEntities } from './lib/utils'
|
||||
|
||||
const Adapter = (typeOrmConfig, options = {}) => {
|
||||
// Ensure typeOrmConfigObject is normalized to an object
|
||||
const typeOrmConfigObject = (typeof typeOrmConfig === 'string')
|
||||
? adapterConfig.parseConnectionString(typeOrmConfig)
|
||||
: typeOrmConfig
|
||||
|
||||
// Load any custom models passed as an option, default to built in models
|
||||
const { models: customModels = {} } = options
|
||||
const models = {
|
||||
User: customModels.User ? customModels.User : Models.User,
|
||||
Account: customModels.Account ? customModels.Account : Models.Account,
|
||||
Session: customModels.Session ? customModels.Session : Models.Session,
|
||||
VerificationRequest: customModels.VerificationRequest ? customModels.VerificationRequest : Models.VerificationRequest
|
||||
}
|
||||
|
||||
// The models are designed for ANSI SQL databases first (as a baseline).
|
||||
// For databases that use a different pragma, we transform the models at run
|
||||
// time *unless* the models are user supplied (in which case we don't do
|
||||
// anything to do them). This function updates arguments by reference.
|
||||
adapterTransform(typeOrmConfigObject, models, options)
|
||||
|
||||
const config = adapterConfig.loadConfig(typeOrmConfigObject, { ...options, models })
|
||||
|
||||
// Create objects from models that can be consumed by functions in the adapter
|
||||
const User = models.User.model
|
||||
const Account = models.Account.model
|
||||
const Session = models.Session.model
|
||||
const VerificationRequest = models.VerificationRequest.model
|
||||
|
||||
let connection = null
|
||||
|
||||
async function getAdapter (appOptions) {
|
||||
const { logger } = appOptions
|
||||
// Display debug output if debug option enabled
|
||||
function debug (debugCode, ...args) {
|
||||
logger.debug(`TYPEORM_${debugCode}`, ...args)
|
||||
}
|
||||
|
||||
// Helper function to reuse / restablish connections
|
||||
// (useful if they drop when after being idle)
|
||||
async function _connect () {
|
||||
// Get current connection by name
|
||||
connection = getConnection(config.name)
|
||||
|
||||
// If connection is no longer established, reconnect
|
||||
if (!connection.isConnected) { connection = await connection.connect() }
|
||||
}
|
||||
|
||||
if (!connection) {
|
||||
// If no connection, create new connection
|
||||
try {
|
||||
connection = await createConnection(config)
|
||||
} catch (error) {
|
||||
if (error.name === 'AlreadyHasActiveConnectionError') {
|
||||
// If creating connection fails because it's already
|
||||
// been re-established, check it's really up
|
||||
await _connect()
|
||||
} else {
|
||||
logger.error('ADAPTER_CONNECTION_ERROR', error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If the connection object already exists, ensure it's valid
|
||||
await _connect()
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
await updateConnectionEntities(connection, config.entities)
|
||||
}
|
||||
|
||||
// Get manager from connection object
|
||||
// https://github.com/typeorm/typeorm/blob/master/docs/entity-manager-api.md
|
||||
const { manager } = connection
|
||||
|
||||
// The models are primarily designed for ANSI SQL database, but some
|
||||
// flexiblity is required in the adapter to support non-SQL databases such
|
||||
// as MongoDB which have different pragmas.
|
||||
//
|
||||
// TypeORM does some abstraction, but doesn't handle everything (e.g. it
|
||||
// handles translating `id` and `_id` in models, but not queries) so we
|
||||
// need to handle somethings in the adapter to make it compatible.
|
||||
let idKey = 'id'
|
||||
let ObjectId
|
||||
if (config.type === 'mongodb') {
|
||||
idKey = '_id'
|
||||
// Using a dynamic import causes problems for some compilers/bundlers
|
||||
// that don't handle dynamic imports. To try and work around this we are
|
||||
// using the same method mongodb uses to load Object ID type, which is to
|
||||
// use the require_optional loader.
|
||||
const mongodb = require_optional('mongodb')
|
||||
ObjectId = mongodb.ObjectId
|
||||
}
|
||||
|
||||
// These values are stored as seconds, but to use them with dates in
|
||||
// JavaScript we convert them to milliseconds.
|
||||
//
|
||||
// Use a conditional to default to 30 day session age if not set - it should
|
||||
// always be set but a meaningful fallback is helpful to facilitate testing.
|
||||
if (appOptions && (!appOptions.session || !appOptions.session.maxAge)) {
|
||||
debug('GET_ADAPTER', 'Session expiry not configured (defaulting to 30 days')
|
||||
}
|
||||
const defaultSessionMaxAge = 30 * 24 * 60 * 60 * 1000
|
||||
const sessionMaxAge = (appOptions && appOptions.session && appOptions.session.maxAge)
|
||||
? appOptions.session.maxAge * 1000
|
||||
: defaultSessionMaxAge
|
||||
const sessionUpdateAge = (appOptions && appOptions.session && appOptions.session.updateAge)
|
||||
? appOptions.session.updateAge * 1000
|
||||
: 0
|
||||
|
||||
async function createUser (profile) {
|
||||
debug('CREATE_USER', profile)
|
||||
try {
|
||||
// Create user account
|
||||
const user = new User(profile.name, profile.email, profile.image, profile.emailVerified)
|
||||
return await manager.save(user)
|
||||
} catch (error) {
|
||||
logger.error('CREATE_USER_ERROR', error)
|
||||
return Promise.reject(new CreateUserError(error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getUser (id) {
|
||||
debug('GET_USER', id)
|
||||
|
||||
// In the very specific case of both using JWT for storing session data
|
||||
// and using MongoDB to store user data, the ID is a string rather than
|
||||
// an ObjectId and we need to turn it into an ObjectId.
|
||||
//
|
||||
// In all other scenarios it is already an ObjectId, because it will have
|
||||
// come from another MongoDB query.
|
||||
if (ObjectId && !(id instanceof ObjectId)) {
|
||||
id = ObjectId(id)
|
||||
}
|
||||
|
||||
try {
|
||||
return manager.findOne(User, { [idKey]: id })
|
||||
} catch (error) {
|
||||
logger.error('GET_USER_BY_ID_ERROR', error)
|
||||
return Promise.reject(new Error('GET_USER_BY_ID_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getUserByEmail (email) {
|
||||
debug('GET_USER_BY_EMAIL', email)
|
||||
try {
|
||||
if (!email) { return Promise.resolve(null) }
|
||||
return manager.findOne(User, { email })
|
||||
} catch (error) {
|
||||
logger.error('GET_USER_BY_EMAIL_ERROR', error)
|
||||
return Promise.reject(new Error('GET_USER_BY_EMAIL_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getUserByProviderAccountId (providerId, providerAccountId) {
|
||||
debug('GET_USER_BY_PROVIDER_ACCOUNT_ID', providerId, providerAccountId)
|
||||
try {
|
||||
const account = await manager.findOne(Account, { providerId, providerAccountId })
|
||||
if (!account) { return null }
|
||||
return manager.findOne(User, { [idKey]: account.userId })
|
||||
} catch (error) {
|
||||
logger.error('GET_USER_BY_PROVIDER_ACCOUNT_ID_ERROR', error)
|
||||
return Promise.reject(new Error('GET_USER_BY_PROVIDER_ACCOUNT_ID_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function updateUser (user) {
|
||||
debug('UPDATE_USER', user)
|
||||
return manager.save(User, user)
|
||||
}
|
||||
|
||||
async function deleteUser (userId) {
|
||||
debug('DELETE_USER', userId)
|
||||
// @TODO Delete user from DB
|
||||
return false
|
||||
}
|
||||
|
||||
async function linkAccount (userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires) {
|
||||
debug('LINK_ACCOUNT', userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires)
|
||||
try {
|
||||
// Create provider account linked to user
|
||||
const account = new Account(userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires)
|
||||
return manager.save(account)
|
||||
} catch (error) {
|
||||
logger.error('LINK_ACCOUNT_ERROR', error)
|
||||
return Promise.reject(new Error('LINK_ACCOUNT_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function unlinkAccount (userId, providerId, providerAccountId) {
|
||||
debug('UNLINK_ACCOUNT', userId, providerId, providerAccountId)
|
||||
// @TODO Get current user from DB
|
||||
// @TODO Delete [provider] object from user object
|
||||
// @TODO Save changes to user object in DB
|
||||
return false
|
||||
}
|
||||
|
||||
async function createSession (user) {
|
||||
debug('CREATE_SESSION', user)
|
||||
try {
|
||||
let expires = null
|
||||
if (sessionMaxAge) {
|
||||
const dateExpires = new Date()
|
||||
dateExpires.setTime(dateExpires.getTime() + sessionMaxAge)
|
||||
expires = dateExpires
|
||||
}
|
||||
|
||||
const session = new Session(user.id, expires)
|
||||
|
||||
return manager.save(session)
|
||||
} catch (error) {
|
||||
logger.error('CREATE_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('CREATE_SESSION_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getSession (sessionToken) {
|
||||
debug('GET_SESSION', sessionToken)
|
||||
try {
|
||||
const session = await manager.findOne(Session, { sessionToken })
|
||||
|
||||
// Check session has not expired (do not return it if it has)
|
||||
if (session && session.expires && new Date() > new Date(session.expires)) {
|
||||
// @TODO Delete old sessions from database
|
||||
return null
|
||||
}
|
||||
|
||||
return session
|
||||
} catch (error) {
|
||||
logger.error('GET_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('GET_SESSION_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSession (session, force) {
|
||||
debug('UPDATE_SESSION', session)
|
||||
try {
|
||||
if (sessionMaxAge && (sessionUpdateAge || sessionUpdateAge === 0) && session.expires) {
|
||||
// Calculate last updated date, to throttle write updates to database
|
||||
// Formula: ({expiry date} - sessionMaxAge) + sessionUpdateAge
|
||||
// e.g. ({expiry date} - 30 days) + 1 hour
|
||||
//
|
||||
// Default for sessionMaxAge is 30 days.
|
||||
// Default for sessionUpdateAge is 1 hour.
|
||||
const dateSessionIsDueToBeUpdated = new Date(session.expires)
|
||||
dateSessionIsDueToBeUpdated.setTime(dateSessionIsDueToBeUpdated.getTime() - sessionMaxAge)
|
||||
dateSessionIsDueToBeUpdated.setTime(dateSessionIsDueToBeUpdated.getTime() + sessionUpdateAge)
|
||||
|
||||
// Trigger update of session expiry date and write to database, only
|
||||
// if the session was last updated more than {sessionUpdateAge} ago
|
||||
if (new Date() > dateSessionIsDueToBeUpdated) {
|
||||
const newExpiryDate = new Date()
|
||||
newExpiryDate.setTime(newExpiryDate.getTime() + sessionMaxAge)
|
||||
session.expires = newExpiryDate
|
||||
} else if (!force) {
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
// If session MaxAge, session UpdateAge or session.expires are
|
||||
// missing then don't even try to save changes, unless force is set.
|
||||
if (!force) { return null }
|
||||
}
|
||||
|
||||
return manager.save(Session, session)
|
||||
} catch (error) {
|
||||
logger.error('UPDATE_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('UPDATE_SESSION_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSession (sessionToken) {
|
||||
debug('DELETE_SESSION', sessionToken)
|
||||
try {
|
||||
return await manager.delete(Session, { sessionToken })
|
||||
} catch (error) {
|
||||
logger.error('DELETE_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('DELETE_SESSION_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function createVerificationRequest (identifier, url, token, secret, provider) {
|
||||
debug('CREATE_VERIFICATION_REQUEST', identifier)
|
||||
try {
|
||||
const { baseUrl } = appOptions
|
||||
const { sendVerificationRequest, maxAge } = provider
|
||||
|
||||
// Store hashed token (using secret as salt) so that tokens cannot be exploited
|
||||
// even if the contents of the database is compromised.
|
||||
// @TODO Use bcrypt function here instead of simple salted hash
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
|
||||
let expires = null
|
||||
if (maxAge) {
|
||||
const dateExpires = new Date()
|
||||
dateExpires.setTime(dateExpires.getTime() + (maxAge * 1000))
|
||||
expires = dateExpires
|
||||
}
|
||||
|
||||
// Save to database
|
||||
const newVerificationRequest = new VerificationRequest(identifier, hashedToken, expires)
|
||||
const verificationRequest = await manager.save(newVerificationRequest)
|
||||
|
||||
// With the verificationCallback on a provider, you can send an email, or queue
|
||||
// an email to be sent, or perform some other action (e.g. send a text message)
|
||||
await sendVerificationRequest({ identifier, url, token, baseUrl, provider })
|
||||
|
||||
return verificationRequest
|
||||
} catch (error) {
|
||||
logger.error('CREATE_VERIFICATION_REQUEST_ERROR', error)
|
||||
return Promise.reject(new Error('CREATE_VERIFICATION_REQUEST_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getVerificationRequest (identifier, token, secret, provider) {
|
||||
debug('GET_VERIFICATION_REQUEST', identifier, token)
|
||||
try {
|
||||
// Hash token provided with secret before trying to match it with database
|
||||
// @TODO Use bcrypt instead of salted SHA-256 hash for token
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
const verificationRequest = await manager.findOne(VerificationRequest, { identifier, token: hashedToken })
|
||||
|
||||
if (verificationRequest && verificationRequest.expires && new Date() > new Date(verificationRequest.expires)) {
|
||||
// Delete verification entry so it cannot be used again
|
||||
await manager.delete(VerificationRequest, { identifier, token: hashedToken })
|
||||
return null
|
||||
}
|
||||
|
||||
return verificationRequest
|
||||
} catch (error) {
|
||||
logger.error('GET_VERIFICATION_REQUEST_ERROR', error)
|
||||
return Promise.reject(new Error('GET_VERIFICATION_REQUEST_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteVerificationRequest (identifier, token, secret, provider) {
|
||||
debug('DELETE_VERIFICATION', identifier, token)
|
||||
try {
|
||||
// Delete verification entry so it cannot be used again
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
await manager.delete(VerificationRequest, { identifier, token: hashedToken })
|
||||
} catch (error) {
|
||||
logger.error('DELETE_VERIFICATION_REQUEST_ERROR', error)
|
||||
return Promise.reject(new Error('DELETE_VERIFICATION_REQUEST_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
createUser,
|
||||
getUser,
|
||||
getUserByEmail,
|
||||
getUserByProviderAccountId,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
linkAccount,
|
||||
unlinkAccount,
|
||||
createSession,
|
||||
getSession,
|
||||
updateSession,
|
||||
deleteSession,
|
||||
createVerificationRequest,
|
||||
getVerificationRequest,
|
||||
deleteVerificationRequest
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
getAdapter
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
Adapter,
|
||||
Models
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import { EntitySchema } from 'typeorm'
|
||||
|
||||
const parseConnectionString = (configString) => {
|
||||
if (typeof configString !== 'string') { return configString }
|
||||
|
||||
// If the input is URL string, automatically convert the string to an object
|
||||
// to make configuration easier (in most use cases).
|
||||
//
|
||||
// TypeORM accepts connection string as a 'url' option, but unfortunately
|
||||
// not for all databases (e.g. SQLite) or for all options, so we handle
|
||||
// parsing it in this function.
|
||||
try {
|
||||
const parsedUrl = new URL(configString)
|
||||
const config = {}
|
||||
|
||||
if (parsedUrl.protocol.startsWith('mongodb+srv')) {
|
||||
// Special case handling is required for mongodb+srv with TypeORM
|
||||
config.type = 'mongodb'
|
||||
config.url = configString.replace(/\?(.*)$/, '')
|
||||
config.useNewUrlParser = true
|
||||
} else {
|
||||
config.type = parsedUrl.protocol.replace(/:$/, '')
|
||||
config.host = parsedUrl.hostname
|
||||
config.port = Number(parsedUrl.port)
|
||||
config.username = parsedUrl.username
|
||||
config.password = parsedUrl.password
|
||||
config.database = parsedUrl.pathname.replace(/^\//, '').replace(/\?(.*)$/, '')
|
||||
config.options = {}
|
||||
}
|
||||
|
||||
// This option is recommended by mongodb
|
||||
if (config.type === 'mongodb') {
|
||||
config.useUnifiedTopology = true
|
||||
}
|
||||
|
||||
// Prevents warning about deprecated option (sets default value)
|
||||
if (config.type === 'mssql') {
|
||||
config.options.enableArithAbort = true
|
||||
}
|
||||
|
||||
if (parsedUrl.search) {
|
||||
parsedUrl.search.replace(/^\?/, '').split('&').forEach(keyValuePair => {
|
||||
let [key, value] = keyValuePair.split('=')
|
||||
// Converts true/false strings to actual boolean values
|
||||
if (value === 'true') { value = true }
|
||||
if (value === 'false') { value = false }
|
||||
config[key] = value
|
||||
})
|
||||
}
|
||||
|
||||
return config
|
||||
} catch (error) {
|
||||
// If URL parsing fails for any reason, try letting TypeORM handle it
|
||||
return {
|
||||
url: configString
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const loadConfig = (config, { models, namingStrategy }) => {
|
||||
const defaultConfig = {
|
||||
name: 'nextauth',
|
||||
autoLoadEntities: true,
|
||||
entities: [
|
||||
new EntitySchema(models.User.schema),
|
||||
new EntitySchema(models.Account.schema),
|
||||
new EntitySchema(models.Session.schema),
|
||||
new EntitySchema(models.VerificationRequest.schema)
|
||||
],
|
||||
timezone: 'Z', // Required for timestamps to be treated as UTC in MySQL
|
||||
logging: false,
|
||||
namingStrategy
|
||||
}
|
||||
|
||||
return {
|
||||
...defaultConfig,
|
||||
...config
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
parseConnectionString,
|
||||
loadConfig
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
// Inspired by https://github.com/tonivj5/typeorm-naming-strategies
|
||||
import { DefaultNamingStrategy } from 'typeorm'
|
||||
import { snakeCase, camelCase } from 'typeorm/util/StringUtils'
|
||||
|
||||
export class SnakeCaseNamingStrategy extends DefaultNamingStrategy {
|
||||
// Pluralise table names (set customName to override)
|
||||
tableName (className, customName) {
|
||||
return customName || snakeCase(`${className}s`)
|
||||
}
|
||||
|
||||
columnName (propertyName, customName, embeddedPrefixes) {
|
||||
return `${snakeCase(embeddedPrefixes.join('_'))}${customName || snakeCase(propertyName)}`
|
||||
}
|
||||
|
||||
relationName (propertyName) {
|
||||
return snakeCase(propertyName)
|
||||
}
|
||||
|
||||
joinColumnName (relationName, referencedColumnName) {
|
||||
return snakeCase(`${relationName}_${referencedColumnName}`)
|
||||
}
|
||||
|
||||
joinTableName (firstTableName, secondTableName, firstPropertyName, secondPropertyName) {
|
||||
return snakeCase(`${firstTableName}_${firstPropertyName.replace(/\./gi, '_')}_${secondTableName}`)
|
||||
}
|
||||
|
||||
joinTableColumnName (tableName, propertyName, columnName) {
|
||||
return snakeCase(`${tableName}_${(columnName || propertyName)}`)
|
||||
}
|
||||
|
||||
classTableInheritanceParentColumnName (parentTableName, parentTableIdPropertyName) {
|
||||
return snakeCase(`${parentTableName}_${parentTableIdPropertyName}`)
|
||||
}
|
||||
|
||||
eagerJoinRelationAlias (alias, propertyPath) {
|
||||
return `${alias}__${propertyPath.replace('.', '_')}`
|
||||
}
|
||||
}
|
||||
|
||||
export class CamelCaseNamingStrategy extends DefaultNamingStrategy {
|
||||
// Pluralise collection names, uses (set customName to override)
|
||||
tableName (className, customName) {
|
||||
return customName || camelCase(`${className}s`)
|
||||
}
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
// Perform transforms on SQL models so they can be used with other databases
|
||||
import { SnakeCaseNamingStrategy, CamelCaseNamingStrategy } from './naming-strategies'
|
||||
|
||||
const postgresTransform = (models, options) => {
|
||||
// Apply snake case naming strategy for Postgres databases
|
||||
if (!options.namingStrategy) {
|
||||
options.namingStrategy = new SnakeCaseNamingStrategy()
|
||||
}
|
||||
|
||||
// For Postgres we need to use the `timestamp with time zone` type
|
||||
// aka `timestamptz` to store timestamps correctly in UTC.
|
||||
for (const model in models) {
|
||||
for (const column in models[model].schema.columns) {
|
||||
if (models[model].schema.columns[column].type === 'timestamp') {
|
||||
models[model].schema.columns[column].type = 'timestamptz'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mysqlTransform = (models, options) => {
|
||||
// Apply snake case naming strategy for MySQL databases
|
||||
if (!options.namingStrategy) {
|
||||
options.namingStrategy = new SnakeCaseNamingStrategy()
|
||||
}
|
||||
|
||||
// For MySQL we default milisecond precision of all timestamps to 6 digits.
|
||||
// This ensures all timestamp fields use the same precision (unless explictly
|
||||
// configured otherwise) and that values in MySQL match those Postgress.
|
||||
for (const model in models) {
|
||||
for (const column in models[model].schema.columns) {
|
||||
if (models[model].schema.columns[column].type === 'timestamp') {
|
||||
// If precision explictly set (including to null) don't change it
|
||||
if (typeof models[model].schema.columns[column].precision === 'undefined') {
|
||||
models[model].schema.columns[column].precision = 6
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mongodbTransform = (models, options) => {
|
||||
// A CamelCase naming strategy is used for all document databases
|
||||
if (!options.namingStrategy) {
|
||||
options.namingStrategy = new CamelCaseNamingStrategy()
|
||||
}
|
||||
|
||||
// Important!
|
||||
//
|
||||
// 1. You must set 'objectId: true' on one property on a model in MongoDB.
|
||||
//
|
||||
// 'objectId' MUST be set on the primary ID field. This overrides other
|
||||
// values on that object in TypeORM (e.g. type: 'int' or 'primary').
|
||||
//
|
||||
// 2. Other properties that are Object IDs in the same model MUST be set to
|
||||
// type: 'objectId' (and should not be set to `objectId: true`).
|
||||
//
|
||||
// If you set 'objectId: true' on multiple properties on a model you will
|
||||
// see the result of queries like find() is wrong. You will see the same
|
||||
// Object ID in every property of type Object ID in the result (but the
|
||||
// database will look fine); so use `type: 'objectId'` for them instead.
|
||||
for (const model in models) {
|
||||
delete models[model].schema.columns.id.type
|
||||
models[model].schema.columns.id.objectId = true
|
||||
}
|
||||
|
||||
// Ensure reference to User ID in other models are Object IDs
|
||||
// This needs to done for any properties that reference another entity by ID
|
||||
models.Account.schema.columns.userId.type = 'objectId'
|
||||
models.Session.schema.columns.userId.type = 'objectId'
|
||||
|
||||
// The options `unique: true` and `nullable: true` don't work the same
|
||||
// with MongoDB as they do with SQL databases like MySQL and Postgres,
|
||||
// we need to create a sparse index to only allow unique values, while
|
||||
// still allowing multiple entires to omit the email address.
|
||||
delete models.User.schema.columns.email.unique
|
||||
|
||||
if (!models.User.schema.indices) { models.User.schema.indices = [] }
|
||||
|
||||
models.User.schema.indices.push({
|
||||
name: 'email',
|
||||
unique: true,
|
||||
sparse: true,
|
||||
columns: ['email']
|
||||
})
|
||||
}
|
||||
|
||||
const sqliteTransform = (models, options) => {
|
||||
// Apply snake case naming strategy for SQLite databases
|
||||
if (!options.namingStrategy) {
|
||||
options.namingStrategy = new SnakeCaseNamingStrategy()
|
||||
}
|
||||
|
||||
// SQLite does not support `timestamp` fields so we remap them to `datetime`
|
||||
// in all models.
|
||||
//
|
||||
// `timestamp` is an ANSI SQL specification and widely supported by other
|
||||
// databases so this transform is a specific workaround required for SQLite.
|
||||
//
|
||||
// NB: SQLite adds 'create' and 'update' fields to allow rows, but that is
|
||||
// specific to SQLite and so we ignore that behaviour.
|
||||
for (const model in models) {
|
||||
for (const column in models[model].schema.columns) {
|
||||
if (models[model].schema.columns[column].type === 'timestamp') {
|
||||
models[model].schema.columns[column].type = 'datetime'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mssqlTransform = (models, options) => {
|
||||
// Apply snake case naming strategy for SQL Server databases
|
||||
if (!options.namingStrategy) {
|
||||
// @TODO Add TitleCase instead as more common MSSQL convention?
|
||||
options.namingStrategy = new SnakeCaseNamingStrategy()
|
||||
}
|
||||
|
||||
// SQL Server deprecated TIMESTAMP in favor of ROWVERSION.
|
||||
// But ROWVERSION is not what it was intended in the other adapters.
|
||||
for (const model in models) {
|
||||
for (const column in models[model].schema.columns) {
|
||||
if (models[model].schema.columns[column].type === 'timestamp') {
|
||||
models[model].schema.columns[column].type = 'datetime'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Support UNIQUE on on User.email that allows duplicate NULL values
|
||||
// Note: This is ANSI SQL behaviour for UNIQUE not default in SQL Server
|
||||
delete models.User.schema.columns.email.unique
|
||||
|
||||
if (!models.User.schema.indices) { models.User.schema.indices = [] }
|
||||
|
||||
models.User.schema.indices.push({
|
||||
name: 'email',
|
||||
columns: ['email'],
|
||||
unique: true,
|
||||
where: 'email IS NOT NULL'
|
||||
})
|
||||
}
|
||||
|
||||
export default (config, models, options) => {
|
||||
// @TODO Refactor into switch statement
|
||||
if ((config.type && config.type.startsWith('mongodb')) ||
|
||||
(config.url && config.url.startsWith('mongodb'))) {
|
||||
mongodbTransform(models, options)
|
||||
} else if ((config.type && config.type.startsWith('postgres')) ||
|
||||
(config.url && config.url.startsWith('postgres'))) {
|
||||
postgresTransform(models, options)
|
||||
} else if ((config.type && config.type.startsWith('mysql')) ||
|
||||
(config.url && config.url.startsWith('mysql'))) {
|
||||
mysqlTransform(models, options)
|
||||
} else if ((config.type && config.type.startsWith('sqlite')) ||
|
||||
(config.url && config.url.startsWith('sqlite'))) {
|
||||
sqliteTransform(models, options)
|
||||
} else if ((config.type && config.type.startsWith('mssql')) ||
|
||||
(config.url && config.url.startsWith('mssql'))) {
|
||||
mssqlTransform(models, options)
|
||||
} else {
|
||||
// For all other SQL databases (e.g. MySQL) apply snake case naming
|
||||
// strategy, but otherwise use the models and schemas as they are.
|
||||
if (!options.namingStrategy) {
|
||||
options.namingStrategy = new SnakeCaseNamingStrategy()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
const entitiesChanged = (prevEntities, newEntities) => {
|
||||
if (prevEntities.length !== newEntities.length) return true
|
||||
for (let i = 0; i < prevEntities.length; i++) {
|
||||
if (prevEntities[i] !== newEntities[i]) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export const updateConnectionEntities = async (connection, entities) => {
|
||||
// Check if the entities passed have changed and if so replace them
|
||||
// and re-sync the typeorm connection.
|
||||
if (!connection || !entitiesChanged(connection.options.entities, entities)) return
|
||||
connection.options.entities = entities
|
||||
connection.buildMetadatas()
|
||||
if (connection.options.synchronize) {
|
||||
await connection.synchronize()
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import { createHash } from 'crypto'
|
||||
|
||||
export class Account {
|
||||
constructor (
|
||||
userId,
|
||||
providerId,
|
||||
providerType,
|
||||
providerAccountId,
|
||||
refreshToken,
|
||||
accessToken,
|
||||
accessTokenExpires
|
||||
) {
|
||||
// The compound ID ensures there is only one entry for a given provider and account
|
||||
this.compoundId = createHash('sha256').update(`${providerId}:${providerAccountId}`).digest('hex')
|
||||
this.userId = userId
|
||||
this.providerType = providerType
|
||||
this.providerId = providerId
|
||||
this.providerAccountId = providerAccountId
|
||||
this.refreshToken = refreshToken
|
||||
this.accessToken = accessToken
|
||||
this.accessTokenExpires = accessTokenExpires
|
||||
}
|
||||
}
|
||||
|
||||
export const AccountSchema = {
|
||||
name: 'Account',
|
||||
target: Account,
|
||||
columns: {
|
||||
id: {
|
||||
// This property has `objectId: true` instead of `type: int` in MongoDB
|
||||
primary: true,
|
||||
type: 'int',
|
||||
generated: true
|
||||
},
|
||||
compoundId: {
|
||||
// The compound ID ensures that there there is only one instance of an
|
||||
// OAuth account in a way that works across different databases.
|
||||
// It is not used for anything else.
|
||||
type: 'varchar',
|
||||
unique: true
|
||||
},
|
||||
userId: {
|
||||
// This property is set to `type: objectId` on MongoDB databases
|
||||
type: 'int'
|
||||
},
|
||||
providerType: {
|
||||
type: 'varchar'
|
||||
},
|
||||
providerId: {
|
||||
type: 'varchar'
|
||||
},
|
||||
providerAccountId: {
|
||||
type: 'varchar'
|
||||
},
|
||||
refreshToken: {
|
||||
type: 'text',
|
||||
nullable: true
|
||||
},
|
||||
accessToken: {
|
||||
// AccessTokens are not (yet) automatically rotated by NextAuth.js
|
||||
// You can update it using the refreshToken and the accessTokenUrl endpoint for the provider
|
||||
type: 'text',
|
||||
nullable: true
|
||||
},
|
||||
accessTokenExpires: {
|
||||
// AccessTokens expiry times are not (yet) updated by NextAuth.js
|
||||
// You can update it using the refreshToken and the accessTokenUrl endpoint for the provider
|
||||
type: 'timestamp',
|
||||
nullable: true
|
||||
},
|
||||
createdAt: {
|
||||
type: 'timestamp',
|
||||
createDate: true
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'timestamp',
|
||||
updateDate: true
|
||||
}
|
||||
},
|
||||
indices: [
|
||||
{
|
||||
name: 'userId',
|
||||
columns: ['userId']
|
||||
},
|
||||
{
|
||||
name: 'providerId',
|
||||
columns: ['providerId']
|
||||
},
|
||||
{
|
||||
name: 'providerAccountId',
|
||||
columns: ['providerAccountId']
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Account, AccountSchema } from './account'
|
||||
import { User, UserSchema } from './user'
|
||||
import { Session, SessionSchema } from './session'
|
||||
import { VerificationRequest, VerificationRequestSchema } from './verification-request'
|
||||
|
||||
export default {
|
||||
Account: {
|
||||
model: Account,
|
||||
schema: AccountSchema
|
||||
},
|
||||
User: {
|
||||
model: User,
|
||||
schema: UserSchema
|
||||
},
|
||||
Session: {
|
||||
model: Session,
|
||||
schema: SessionSchema
|
||||
},
|
||||
VerificationRequest: {
|
||||
model: VerificationRequest,
|
||||
schema: VerificationRequestSchema
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { randomBytes } from 'crypto'
|
||||
|
||||
export class Session {
|
||||
constructor (userId, expires, sessionToken, accessToken) {
|
||||
this.userId = userId
|
||||
this.expires = expires
|
||||
this.sessionToken = sessionToken || randomBytes(32).toString('hex')
|
||||
this.accessToken = accessToken || randomBytes(32).toString('hex')
|
||||
}
|
||||
}
|
||||
|
||||
export const SessionSchema = {
|
||||
name: 'Session',
|
||||
target: Session,
|
||||
columns: {
|
||||
id: {
|
||||
// This property has `objectId: true` instead of `type: int` in MongoDB
|
||||
primary: true,
|
||||
type: 'int',
|
||||
generated: true
|
||||
},
|
||||
userId: {
|
||||
// This property is set to `type: objectId` on MongoDB databases
|
||||
type: 'int'
|
||||
},
|
||||
expires: {
|
||||
// The date the session expires (is updated when a session is active)
|
||||
type: 'timestamp'
|
||||
},
|
||||
sessionToken: {
|
||||
// The sessionToken should never be exposed to client side JavaScript
|
||||
type: 'varchar',
|
||||
unique: true
|
||||
},
|
||||
accessToken: {
|
||||
// The accessToken can be safely exposed to client side JavaScript to
|
||||
// to identify the owner of a session without exposing the sessionToken
|
||||
type: 'varchar',
|
||||
unique: true
|
||||
},
|
||||
createdAt: {
|
||||
type: 'timestamp',
|
||||
createDate: true
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'timestamp',
|
||||
updateDate: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
export class User {
|
||||
constructor (name, email, image, emailVerified) {
|
||||
if (name) { this.name = name }
|
||||
if (email) { this.email = email }
|
||||
if (image) { this.image = image }
|
||||
if (emailVerified) {
|
||||
const currentDate = new Date()
|
||||
this.emailVerified = currentDate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const UserSchema = {
|
||||
name: 'User',
|
||||
target: User,
|
||||
columns: {
|
||||
id: {
|
||||
// This property has `objectId: true` instead of `type: int` in MongoDB
|
||||
primary: true,
|
||||
type: 'int',
|
||||
generated: true
|
||||
},
|
||||
name: {
|
||||
type: 'varchar',
|
||||
nullable: true
|
||||
},
|
||||
email: {
|
||||
// This is inherited from the one in the OAuth provider profile on
|
||||
// initial sign in, if one is specified in that profile.
|
||||
type: 'varchar',
|
||||
unique: true,
|
||||
nullable: true
|
||||
},
|
||||
emailVerified: {
|
||||
// Contains a timestamp of the last time an action was performed that
|
||||
// confirmed this email address was active and used by the user (e.g.
|
||||
// when an email sign in link is clicked on and verified). Is null
|
||||
// if the email address specified has never been verified.
|
||||
type: 'timestamp',
|
||||
nullable: true
|
||||
},
|
||||
image: {
|
||||
// A URL that points to an avatar to use for the user.
|
||||
// This is inherited from the one in the OAuth provider profile on
|
||||
// initial sign in, if one is specified in that profile.
|
||||
type: 'varchar',
|
||||
nullable: true
|
||||
},
|
||||
createdAt: {
|
||||
type: 'timestamp',
|
||||
createDate: true
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'timestamp',
|
||||
updateDate: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
// This model is used for sign in emails, but is designed to support other
|
||||
// mechanisms in future (e.g. 2FA via text message or short codes)
|
||||
export class VerificationRequest {
|
||||
constructor (identifier, token, expires) {
|
||||
if (identifier) { this.identifier = identifier }
|
||||
if (token) { this.token = token }
|
||||
if (expires) { this.expires = expires }
|
||||
}
|
||||
}
|
||||
|
||||
export const VerificationRequestSchema = {
|
||||
name: 'VerificationRequest',
|
||||
target: VerificationRequest,
|
||||
columns: {
|
||||
id: {
|
||||
// This property has `objectId: true` instead of `type: int` in MongoDB
|
||||
primary: true,
|
||||
type: 'int',
|
||||
generated: true
|
||||
},
|
||||
identifier: {
|
||||
// An email address, phone number, username or other unique identifier
|
||||
// associated with the request (used to track who it was on behalf of)
|
||||
type: 'varchar'
|
||||
},
|
||||
token: {
|
||||
// The token used verify the request (maybe hashed or encrypted)
|
||||
type: 'varchar',
|
||||
unique: true
|
||||
},
|
||||
expires: {
|
||||
// After this time, the request will no longer ve valid
|
||||
type: 'timestamp'
|
||||
},
|
||||
createdAt: {
|
||||
type: 'timestamp',
|
||||
createDate: true
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'timestamp',
|
||||
updateDate: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -355,6 +355,22 @@ function BroadcastChannel (name = 'nextauth.message') {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Some methods are exported with more than one name. This provides some
|
||||
// flexibility over how they can be invoked and backwards compatibility
|
||||
// with earlier releases. These should be removed in a newer release, as it only
|
||||
// creates problems for bundlers and adds confusion to users. TypeScript declarations
|
||||
// will provide sufficient help when importing
|
||||
export {
|
||||
setOptions as options,
|
||||
getSession as session,
|
||||
getProviders as providers,
|
||||
getCsrfToken as csrfToken,
|
||||
signIn as signin,
|
||||
signOut as signout
|
||||
}
|
||||
|
||||
export default {
|
||||
getSession,
|
||||
getCsrfToken,
|
||||
|
||||
@@ -1,39 +1,98 @@
|
||||
/**
|
||||
* Same as the default `Error`, but it is JSON serializable.
|
||||
* @source https://iaincollins.medium.com/error-handling-in-javascript-a6172ccdf9af
|
||||
*/
|
||||
export class UnknownError extends Error {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'UnknownError'
|
||||
constructor(error) {
|
||||
// Support passing error or string
|
||||
super(error?.message ?? error)
|
||||
this.name = "UnknownError"
|
||||
if (error instanceof Error) {
|
||||
this.stack = error.stack
|
||||
}
|
||||
}
|
||||
|
||||
toJSON () {
|
||||
toJSON() {
|
||||
return {
|
||||
error: {
|
||||
name: this.name,
|
||||
message: this.message
|
||||
// stack: this.stack
|
||||
}
|
||||
name: this.name,
|
||||
message: this.message,
|
||||
stack: this.stack,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class CreateUserError extends UnknownError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'CreateUserError'
|
||||
}
|
||||
}
|
||||
|
||||
// Thrown when an Email address is already associated with an account
|
||||
// but the user is trying an OAuth account that is not linked to it.
|
||||
export class AccountNotLinkedError extends UnknownError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'AccountNotLinkedError'
|
||||
}
|
||||
}
|
||||
|
||||
export class OAuthCallbackError extends UnknownError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'OAuthCallbackError'
|
||||
}
|
||||
name = "OAuthCallbackError"
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when an Email address is already associated with an account
|
||||
* but the user is trying an OAuth account that is not linked to it.
|
||||
*/
|
||||
export class AccountNotLinkedError extends UnknownError {
|
||||
name = "AccountNotLinkedError"
|
||||
}
|
||||
|
||||
export class CreateUserError extends UnknownError {
|
||||
name = "CreateUserError"
|
||||
}
|
||||
|
||||
export class GetUserError extends UnknownError {
|
||||
name = "GetUserError"
|
||||
}
|
||||
|
||||
export class GetUserByEmailError extends UnknownError {
|
||||
name = "GetUserByEmailError"
|
||||
}
|
||||
|
||||
export class GetUserByIdError extends UnknownError {
|
||||
name = "GetUserByIdError"
|
||||
}
|
||||
|
||||
export class GetUserByProviderAccountIdError extends UnknownError {
|
||||
name = "GetUserByProviderAccountIdError"
|
||||
}
|
||||
|
||||
export class UpdateUserError extends UnknownError {
|
||||
name = "UpdateUserError"
|
||||
}
|
||||
|
||||
export class DeleteUserError extends UnknownError {
|
||||
name = "DeleteUserError"
|
||||
}
|
||||
|
||||
export class LinkAccountError extends UnknownError {
|
||||
name = "LinkAccountError"
|
||||
}
|
||||
|
||||
export class UnlinkAccountError extends UnknownError {
|
||||
name = "UnlinkAccountError"
|
||||
}
|
||||
|
||||
export class CreateSessionError extends UnknownError {
|
||||
name = "CreateSessionError"
|
||||
}
|
||||
|
||||
export class GetSessionError extends UnknownError {
|
||||
name = "GetSessionError"
|
||||
}
|
||||
|
||||
export class UpdateSessionError extends UnknownError {
|
||||
name = "UpdateSessionError"
|
||||
}
|
||||
|
||||
export class DeleteSessionError extends UnknownError {
|
||||
name = "DeleteSessionError"
|
||||
}
|
||||
|
||||
export class CreateVerificationRequestError extends UnknownError {
|
||||
name = "CreateVerificationRequestError"
|
||||
}
|
||||
|
||||
export class GetVerificationRequestError extends UnknownError {
|
||||
name = "GetVerificationRequestError"
|
||||
}
|
||||
|
||||
export class DeleteVerificationRequestError extends UnknownError {
|
||||
name = "DeleteVerificationRequestError"
|
||||
}
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import crypto from 'crypto'
|
||||
import jose from 'jose'
|
||||
import logger from './logger'
|
||||
import crypto from "crypto"
|
||||
import jose from "jose"
|
||||
import logger from "./logger"
|
||||
|
||||
// Set default algorithm to use for auto-generated signing key
|
||||
const DEFAULT_SIGNATURE_ALGORITHM = 'HS512'
|
||||
const DEFAULT_SIGNATURE_ALGORITHM = "HS512"
|
||||
|
||||
// Set default algorithm for auto-generated symmetric encryption key
|
||||
const DEFAULT_ENCRYPTION_ALGORITHM = 'A256GCM'
|
||||
const DEFAULT_ENCRYPTION_ALGORITHM = "A256GCM"
|
||||
|
||||
// Use encryption or not by default
|
||||
const DEFAULT_ENCRYPTION_ENABLED = false
|
||||
|
||||
const DEFAULT_MAX_AGE = 30 * 24 * 60 * 60 // 30 days
|
||||
|
||||
async function encode ({
|
||||
export async function encode({
|
||||
token = {},
|
||||
maxAge = DEFAULT_MAX_AGE,
|
||||
secret,
|
||||
signingKey,
|
||||
signingOptions = {
|
||||
expiresIn: `${maxAge}s`
|
||||
expiresIn: `${maxAge}s`,
|
||||
},
|
||||
encryptionKey,
|
||||
encryptionOptions = {
|
||||
alg: 'dir',
|
||||
alg: "dir",
|
||||
enc: DEFAULT_ENCRYPTION_ALGORITHM,
|
||||
zip: 'DEF'
|
||||
zip: "DEF",
|
||||
},
|
||||
encryption = DEFAULT_ENCRYPTION_ENABLED
|
||||
encryption = DEFAULT_ENCRYPTION_ENABLED,
|
||||
} = {}) {
|
||||
// Signing Key
|
||||
const _signingKey = signingKey
|
||||
@@ -49,7 +49,7 @@ async function encode ({
|
||||
return signedToken
|
||||
}
|
||||
|
||||
async function decode ({
|
||||
export async function decode({
|
||||
secret,
|
||||
token,
|
||||
maxAge = DEFAULT_MAX_AGE,
|
||||
@@ -57,14 +57,14 @@ async function decode ({
|
||||
verificationKey = signingKey, // Optional (defaults to encryptionKey)
|
||||
verificationOptions = {
|
||||
maxTokenAge: `${maxAge}s`,
|
||||
algorithms: [DEFAULT_SIGNATURE_ALGORITHM]
|
||||
algorithms: [DEFAULT_SIGNATURE_ALGORITHM],
|
||||
},
|
||||
encryptionKey,
|
||||
decryptionKey = encryptionKey, // Optional (defaults to encryptionKey)
|
||||
decryptionOptions = {
|
||||
algorithms: [DEFAULT_ENCRYPTION_ALGORITHM]
|
||||
algorithms: [DEFAULT_ENCRYPTION_ALGORITHM],
|
||||
},
|
||||
encryption = DEFAULT_ENCRYPTION_ENABLED
|
||||
encryption = DEFAULT_ENCRYPTION_ENABLED,
|
||||
} = {}) {
|
||||
if (!token) return null
|
||||
|
||||
@@ -77,8 +77,12 @@ async function decode ({
|
||||
: getDerivedEncryptionKey(secret)
|
||||
|
||||
// Decrypt token
|
||||
const decryptedToken = jose.JWE.decrypt(token, _encryptionKey, decryptionOptions)
|
||||
tokenToVerify = decryptedToken.toString('utf8')
|
||||
const decryptedToken = jose.JWE.decrypt(
|
||||
token,
|
||||
_encryptionKey,
|
||||
decryptionOptions
|
||||
)
|
||||
tokenToVerify = decryptedToken.toString("utf8")
|
||||
}
|
||||
|
||||
// Signing Key
|
||||
@@ -99,17 +103,22 @@ async function decode ({
|
||||
* raw?: boolean
|
||||
* }} params
|
||||
*/
|
||||
async function getToken (params) {
|
||||
export async function getToken(params) {
|
||||
const {
|
||||
req,
|
||||
// Use secure prefix for cookie name, unless URL is NEXTAUTH_URL is http://
|
||||
// or not set (e.g. development or test instance) case use unprefixed name
|
||||
secureCookie = !(!process.env.NEXTAUTH_URL || process.env.NEXTAUTH_URL.startsWith('http://')),
|
||||
cookieName = (secureCookie) ? '__Secure-next-auth.session-token' : 'next-auth.session-token',
|
||||
secureCookie = !(
|
||||
!process.env.NEXTAUTH_URL ||
|
||||
process.env.NEXTAUTH_URL.startsWith("http://")
|
||||
),
|
||||
cookieName = secureCookie
|
||||
? "__Secure-next-auth.session-token"
|
||||
: "next-auth.session-token",
|
||||
raw = false,
|
||||
decode: _decode = decode
|
||||
decode: _decode = decode,
|
||||
} = params
|
||||
if (!req) throw new Error('Must pass `req` to JWT getToken()')
|
||||
if (!req) throw new Error("Must pass `req` to JWT getToken()")
|
||||
|
||||
// Try to get token from cookie
|
||||
let token = req.cookies[cookieName]
|
||||
@@ -117,8 +126,8 @@ async function getToken (params) {
|
||||
// If cookie not found in cookie look for bearer token in authorization header.
|
||||
// This allows clients that pass through tokens in headers rather than as
|
||||
// cookies to use this helper function.
|
||||
if (!token && req.headers.authorization?.split(' ')[0] === 'Bearer') {
|
||||
const urlEncodedToken = req.headers.authorization.split(' ')[1]
|
||||
if (!token && req.headers.authorization?.split(" ")[0] === "Bearer") {
|
||||
const urlEncodedToken = req.headers.authorization.split(" ")[1]
|
||||
token = decodeURIComponent(urlEncodedToken)
|
||||
}
|
||||
|
||||
@@ -138,7 +147,7 @@ let DERIVED_SIGNING_KEY_WARNING = false
|
||||
let DERIVED_ENCRYPTION_KEY_WARNING = false
|
||||
|
||||
// Do the better hkdf of Node.js one added in `v15.0.0` and Third Party one
|
||||
function hkdf (secret, { byteLength, encryptionInfo, digest = 'sha256' }) {
|
||||
function hkdf(secret, { byteLength, encryptionInfo, digest = "sha256" }) {
|
||||
if (crypto.hkdfSync) {
|
||||
return Buffer.from(
|
||||
crypto.hkdfSync(
|
||||
@@ -150,39 +159,50 @@ function hkdf (secret, { byteLength, encryptionInfo, digest = 'sha256' }) {
|
||||
)
|
||||
)
|
||||
}
|
||||
return require('futoin-hkdf')(secret, byteLength, { info: encryptionInfo, hash: digest })
|
||||
return require("futoin-hkdf")(secret, byteLength, {
|
||||
info: encryptionInfo,
|
||||
hash: digest,
|
||||
})
|
||||
}
|
||||
|
||||
function getDerivedSigningKey (secret) {
|
||||
function getDerivedSigningKey(secret) {
|
||||
if (!DERIVED_SIGNING_KEY_WARNING) {
|
||||
logger.warn('JWT_AUTO_GENERATED_SIGNING_KEY')
|
||||
logger.warn("JWT_AUTO_GENERATED_SIGNING_KEY")
|
||||
DERIVED_SIGNING_KEY_WARNING = true
|
||||
}
|
||||
|
||||
const buffer = hkdf(secret, {
|
||||
byteLength: 64,
|
||||
encryptionInfo: 'NextAuth.js Generated Signing Key'
|
||||
encryptionInfo: "NextAuth.js Generated Signing Key",
|
||||
})
|
||||
const key = jose.JWK.asKey(buffer, {
|
||||
alg: DEFAULT_SIGNATURE_ALGORITHM,
|
||||
use: "sig",
|
||||
kid: "nextauth-auto-generated-signing-key",
|
||||
})
|
||||
const key = jose.JWK.asKey(buffer, { alg: DEFAULT_SIGNATURE_ALGORITHM, use: 'sig', kid: 'nextauth-auto-generated-signing-key' })
|
||||
return key
|
||||
}
|
||||
|
||||
function getDerivedEncryptionKey (secret) {
|
||||
function getDerivedEncryptionKey(secret) {
|
||||
if (!DERIVED_ENCRYPTION_KEY_WARNING) {
|
||||
logger.warn('JWT_AUTO_GENERATED_ENCRYPTION_KEY')
|
||||
logger.warn("JWT_AUTO_GENERATED_ENCRYPTION_KEY")
|
||||
DERIVED_ENCRYPTION_KEY_WARNING = true
|
||||
}
|
||||
|
||||
const buffer = hkdf(secret, {
|
||||
byteLength: 32,
|
||||
encryptionInfo: 'NextAuth.js Generated Encryption Key'
|
||||
encryptionInfo: "NextAuth.js Generated Encryption Key",
|
||||
})
|
||||
const key = jose.JWK.asKey(buffer, {
|
||||
alg: DEFAULT_ENCRYPTION_ALGORITHM,
|
||||
use: "enc",
|
||||
kid: "nextauth-auto-generated-encryption-key",
|
||||
})
|
||||
const key = jose.JWK.asKey(buffer, { alg: DEFAULT_ENCRYPTION_ALGORITHM, use: 'enc', kid: 'nextauth-auto-generated-encryption-key' })
|
||||
return key
|
||||
}
|
||||
|
||||
export default {
|
||||
encode,
|
||||
decode,
|
||||
getToken
|
||||
getToken,
|
||||
}
|
||||
|
||||
20
src/providers/42.js
Normal file
20
src/providers/42.js
Normal file
@@ -0,0 +1,20 @@
|
||||
export default function FortyTwo(options) {
|
||||
return {
|
||||
id: '42-school',
|
||||
name: '42 School',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: 'https://api.intra.42.fr/oauth/token',
|
||||
authorizationUrl:
|
||||
'https://api.intra.42.fr/oauth/authorize?response_type=code',
|
||||
profileUrl: 'https://api.intra.42.fr/v2/me',
|
||||
profile: (profile) => ({
|
||||
id: profile.id,
|
||||
email: profile.email,
|
||||
image: profile.image_url,
|
||||
name: profile.usual_full_name,
|
||||
}),
|
||||
...options,
|
||||
}
|
||||
}
|
||||
22
src/providers/mailchimp.js
Normal file
22
src/providers/mailchimp.js
Normal file
@@ -0,0 +1,22 @@
|
||||
export default function Mailchimp(options) {
|
||||
return {
|
||||
id: 'mailchimp',
|
||||
name: 'Mailchimp',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: '',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: 'https://login.mailchimp.com/oauth2/token',
|
||||
authorizationUrl: 'https://login.mailchimp.com/oauth2/authorize?response_type=code',
|
||||
profileUrl: 'https://login.mailchimp.com/oauth2/metadata',
|
||||
profile: (profile) => {
|
||||
return {
|
||||
id: profile.login.login_id,
|
||||
name: profile.accountname,
|
||||
email: profile.login.email,
|
||||
image: null
|
||||
}
|
||||
},
|
||||
...options
|
||||
}
|
||||
}
|
||||
23
src/providers/wordpress.js
Normal file
23
src/providers/wordpress.js
Normal file
@@ -0,0 +1,23 @@
|
||||
export default function WordPress(options) {
|
||||
return {
|
||||
id: "wordpress",
|
||||
name: "WordPress.com",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "auth",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://public-api.wordpress.com/oauth2/token",
|
||||
authorizationUrl:
|
||||
"https://public-api.wordpress.com/oauth2/authorize?response_type=code",
|
||||
profileUrl: "https://public-api.wordpress.com/rest/v1/me",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.ID,
|
||||
name: profile.display_name,
|
||||
email: profile.email,
|
||||
image: profile.avatar_URL,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
# Start test app with local databases inside the container.
|
||||
#
|
||||
# Note: Uses Docker Compose v2 as v3 doesn't currently support extends.
|
||||
# https://docs.docker.com/compose/compose-file/compose-file-v2/
|
||||
version: '2.3'
|
||||
|
||||
services:
|
||||
|
||||
app:
|
||||
build:
|
||||
context: ../../
|
||||
dockerfile: Dockerfile
|
||||
environment:
|
||||
# Set env vars in your current terminal or in .env in the root directory
|
||||
- NEXTAUTH_URL=${NEXTAUTH_URL}
|
||||
- NEXTAUTH_DATABASE_URL=${NEXTAUTH_DATABASE_URL}
|
||||
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
|
||||
- NEXTAUTH_JWT_SESSIONS=${NEXTAUTH_JWT_SESSIONS}
|
||||
- NEXTAUTH_AUTH0_ID=${NEXTAUTH_AUTH0_ID}
|
||||
- NEXTAUTH_AUTH0_SECRET=${NEXTAUTH_AUTH0_SECRET}
|
||||
- NEXTAUTH_AUTH0_DOMAIN=${NEXTAUTH_AUTH0_DOMAIN}
|
||||
- NEXTAUTH_FACEBOOK_ID=${NEXTAUTH_FACEBOOK_ID}
|
||||
- NEXTAUTH_FACEBOOK_SECRET=${NEXTAUTH_FACEBOOK_SECRET}
|
||||
- NEXTAUTH_GITHUB_ID=${NEXTAUTH_GITHUB_ID}
|
||||
- NEXTAUTH_GITHUB_SECRET=${NEXTAUTH_GITHUB_SECRET}
|
||||
- NEXTAUTH_GOOGLE_ID=${NEXTAUTH_GOOGLE_ID}
|
||||
- NEXTAUTH_GOOGLE_SECRET=${NEXTAUTH_GOOGLE_SECRET}
|
||||
- NEXTAUTH_TWITTER_ID=${NEXTAUTH_TWITTER_ID}
|
||||
- NEXTAUTH_TWITTER_SECRET=${NEXTAUTH_TWITTER_SECRET}
|
||||
- NEXTAUTH_EMAIL_SERVER=${NEXTAUTH_EMAIL_SERVER}
|
||||
- NEXTAUTH_EMAIL_FROM=${NEXTAUTH_EMAIL_FROM}
|
||||
ports:
|
||||
- "3000:3000"
|
||||
|
||||
# mongo:
|
||||
# extends:
|
||||
# file: databases/mongo.yml
|
||||
# service: mongo
|
||||
|
||||
# mssql:
|
||||
# extends:
|
||||
# file: databases/mssql.yml
|
||||
# service: mssql
|
||||
|
||||
# mysql:
|
||||
# extends:
|
||||
# file: databases/mysql.yml
|
||||
# service: mysql
|
||||
|
||||
# postgres:
|
||||
# extends:
|
||||
# file: databases/postgres.yml
|
||||
# service: postgres
|
||||
2521
test/docker/app/package-lock.json
generated
2521
test/docker/app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"name": "next-auth-test",
|
||||
"version": "0.0.1",
|
||||
"description": "Test application for NextAuth.js",
|
||||
"main": "",
|
||||
"scripts": {
|
||||
"dev": "next",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"author": "Iain Collins <me@iaincollins.com>",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"next": "^10.0.6",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1"
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Provider } from 'next-auth/client'
|
||||
|
||||
export default function App ({ Component, pageProps }) {
|
||||
return (
|
||||
<Provider
|
||||
options={{
|
||||
// Client Max Age controls how often the useSession in the client should
|
||||
// contact the server to sync the session state. Value in seconds.
|
||||
// e.g.
|
||||
// * 0 - Disabled (always use cache value)
|
||||
// * 60 - Sync session state with server if it's older than 60 seconds
|
||||
clientMaxAge: 0,
|
||||
// Keep Alive tells windows / tabs that are signed in to keep sending
|
||||
// a keep alive request (which extends the current session expiry) to
|
||||
// prevent sessions in open windows from expiring. Value in seconds.
|
||||
//
|
||||
// Note: If a session has expired when keep alive is triggered, all open
|
||||
// windows / tabs will be updated to reflect the user is signed out.
|
||||
keepAlive: 0
|
||||
}}
|
||||
session={pageProps.session}
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
import NextAuth from 'next-auth'
|
||||
import Providers from 'next-auth/providers'
|
||||
|
||||
// For more information on each option (and a full list of options) go to
|
||||
// https://next-auth.js.org/configuration/options
|
||||
const options = {
|
||||
// https://next-auth.js.org/configuration/providers
|
||||
providers: [
|
||||
Providers.Email({
|
||||
server: process.env.NEXTAUTH_EMAIL_SERVER,
|
||||
from: process.env.NEXTAUTH_EMAIL_FROM
|
||||
}),
|
||||
Providers.Apple({
|
||||
clientId: process.env.NEXTAUTH_APPLE_ID,
|
||||
clientSecret: {
|
||||
appleId: process.env.NEXTAUTH_APPLE_ID,
|
||||
teamId: process.env.NEXTAUTH_APPLE_TEAM_ID,
|
||||
privateKey: process.env.NEXTAUTH_APPLE_PRIVATE_KEY,
|
||||
keyId: process.env.NEXTAUTH_APPLE_KEY_ID
|
||||
}
|
||||
}),
|
||||
Providers.Auth0({
|
||||
clientId: process.env.NEXTAUTH_AUTH0_ID,
|
||||
clientSecret: process.env.NEXTAUTH_AUTH0_SECRET,
|
||||
domain: process.env.NEXTAUTH_AUTH0_DOMAIN
|
||||
}),
|
||||
Providers.Facebook({
|
||||
clientId: process.env.NEXTAUTH_FACEBOOK_ID,
|
||||
clientSecret: process.env.NEXTAUTH_FACEBOOK_SECRET
|
||||
}),
|
||||
Providers.GitHub({
|
||||
clientId: process.env.NEXTAUTH_GITHUB_ID,
|
||||
clientSecret: process.env.NEXTAUTH_GITHUB_SECRET
|
||||
}),
|
||||
Providers.Google({
|
||||
clientId: process.env.NEXTAUTH_GOOGLE_ID,
|
||||
clientSecret: process.env.NEXTAUTH_GOOGLE_SECRET
|
||||
}),
|
||||
Providers.Twitter({
|
||||
clientId: process.env.NEXTAUTH_TWITTER_ID,
|
||||
clientSecret: process.env.NEXTAUTH_TWITTER_SECRET
|
||||
})
|
||||
],
|
||||
// Database optional. MySQL, Maria DB, Postgres and MongoDB are supported.
|
||||
// https://next-auth.js.org/configuration/database
|
||||
//
|
||||
// Notes:
|
||||
// * You must to install an appropriate node_module for your database
|
||||
// * The Email provider requires a database (OAuth providers do not)
|
||||
database: process.env.NEXTAUTH_DATABASE_URL,
|
||||
|
||||
// The secret should be set to a reasonably long random string.
|
||||
// It is used to sign cookies and to sign and encrypt JSON Web Tokens, unless
|
||||
// a seperate secret is defined explicitly for encrypting the JWT.
|
||||
secret: process.env.NEXTAUTH_SECRET,
|
||||
|
||||
session: {
|
||||
// Use JSON Web Tokens for session instead of database sessions.
|
||||
// This option can be used with or without a database for users/accounts.
|
||||
// Note: `jwt` is automatically set to `true` if no database is specified.
|
||||
jwt: true
|
||||
|
||||
// Seconds - How long until an idle session expires and is no longer valid.
|
||||
// maxAge: 30 * 24 * 60 * 60, // 30 days
|
||||
|
||||
// Seconds - Throttle how frequently to write to database to extend a session.
|
||||
// Use it to limit write operations. Set to 0 to always update the database.
|
||||
// Note: This option is ignored if using JSON Web Tokens
|
||||
// updateAge: 24 * 60 * 60, // 24 hours
|
||||
},
|
||||
|
||||
// JSON Web tokens are only used for sessions if the `jwt: true` session
|
||||
// option is set - or by default if no database is specified.
|
||||
// https://next-auth.js.org/configuration/options#jwt
|
||||
jwt: {
|
||||
// A secret to use for key generation (you should set this explicitly)
|
||||
// secret: 'INp8IvdIyeMcoGAgFGoA61DdBglwwSqnXJZkgz8PSnw',
|
||||
|
||||
// Set to true to use encryption (default: false)
|
||||
// encryption: true,
|
||||
|
||||
// You can define your own encode/decode functions for signing and encryption
|
||||
// if you want to override the default behaviour.
|
||||
// async encode({ secret, token, maxAge }) {},
|
||||
// async decode({ secret, token, maxAge }) {},
|
||||
},
|
||||
|
||||
// You can define custom pages to override the built-in pages.
|
||||
// The routes shown here are the default URLs that will be used when a custom
|
||||
// pages is not specified for that route.
|
||||
// https://next-auth.js.org/configuration/pages
|
||||
pages: {
|
||||
// signIn: '/api/auth/signin', // Displays signin buttons
|
||||
// signOut: '/api/auth/signout', // Displays form with sign out button
|
||||
// error: '/api/auth/error', // Error code passed in query string as ?error=
|
||||
// verifyRequest: '/api/auth/verify-request', // Used for check email page
|
||||
// newUser: null // If set, new users will be directed here on first sign in
|
||||
},
|
||||
|
||||
// Callbacks are asynchronous functions you can use to control what happens
|
||||
// when an action is performed.
|
||||
// https://next-auth.js.org/configuration/callbacks
|
||||
callbacks: {
|
||||
// async signIn(user, account, profile) { return Promise.resolve(true) },
|
||||
// async redirect(url, baseUrl) { return Promise.resolve(baseUrl) },
|
||||
// async session(session, user) { return Promise.resolve(session) },
|
||||
// async jwt(token, user, account, profile, isNewUser) { return Promise.resolve(token) }
|
||||
},
|
||||
|
||||
// Events are useful for logging
|
||||
// https://next-auth.js.org/configuration/events
|
||||
events: { },
|
||||
|
||||
// Enable debug messages in the console if you are having problems
|
||||
debug: false
|
||||
}
|
||||
|
||||
export default (req, res) => NextAuth(req, res, options)
|
||||
@@ -1,3 +0,0 @@
|
||||
export default (req, res) => {
|
||||
res.send(JSON.stringify(process.env, null, 2))
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import jwt from 'next-auth/jwt'
|
||||
|
||||
const secret = process.env.SECRET
|
||||
|
||||
export default async (req, res) => {
|
||||
const token = await jwt.getToken({ req, secret })
|
||||
res.send(JSON.stringify(token, null, 2))
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { getSession } from 'next-auth/client'
|
||||
|
||||
export default async (req, res) => {
|
||||
const session = await getSession({ req })
|
||||
|
||||
if (session) {
|
||||
res.send({ content: 'Protected content.' })
|
||||
} else {
|
||||
res.send({ content: 'Unprotected content.' })
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { getSession } from 'next-auth/client'
|
||||
|
||||
export default async (req, res) => {
|
||||
const session = await getSession({ req })
|
||||
res.send(JSON.stringify(session, null, 2))
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import Package from 'next-auth/package.json'
|
||||
|
||||
export default (req, res) => {
|
||||
res.send(Package.version)
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export default function IndexPage () {
|
||||
return (
|
||||
<div id='nextauth-test-app'>
|
||||
<h1>NextAuth.js Test App</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { useSession } from 'next-auth/client'
|
||||
|
||||
export default function TestPage () {
|
||||
const [ session, loading ] = useSession()
|
||||
|
||||
return (
|
||||
<div id='nextauth-test-page'>
|
||||
<h1>NextAuth.js Test Page</h1>
|
||||
{session && <p id="nextauth-signed-in">Signed in</p>}
|
||||
{!session && !loading && <p id="nextauth-signed-out">Signed out</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
# Start Mongo, MSSQL, MySQL and Postgres databases on the current host running
|
||||
# on their respective default ports. This is intended for developer convenience
|
||||
# to make it easier to develop and test features manually.
|
||||
#
|
||||
# Note: Uses Docker Compose v2 as v3 doesn't currently support extends.
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
|
||||
mongo:
|
||||
extends:
|
||||
file: databases/mongo.yml
|
||||
service: mongo
|
||||
ports:
|
||||
- "27017:27017"
|
||||
|
||||
mssql:
|
||||
extends:
|
||||
file: databases/mssql.yml
|
||||
service: mssql
|
||||
ports:
|
||||
- "1433:1433"
|
||||
|
||||
mysql:
|
||||
extends:
|
||||
file: databases/mysql.yml
|
||||
service: mysql
|
||||
ports:
|
||||
- "3306:3306"
|
||||
|
||||
postgres:
|
||||
extends:
|
||||
file: databases/postgres.yml
|
||||
service: postgres
|
||||
ports:
|
||||
- "5432:5432"
|
||||
@@ -1,11 +0,0 @@
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
|
||||
mongo:
|
||||
image: bitnami/mongodb
|
||||
restart: always
|
||||
environment:
|
||||
MONGODB_USERNAME: nextauth
|
||||
MONGODB_PASSWORD: password
|
||||
MONGODB_DATABASE: nextauth
|
||||
@@ -1,13 +0,0 @@
|
||||
version: "2"
|
||||
|
||||
services:
|
||||
mssql:
|
||||
image: mcr.microsoft.com/mssql/server:2017-latest
|
||||
restart: always
|
||||
environment:
|
||||
SA_PASSWORD: Pa55w0rd # minimum password complexity
|
||||
ACCEPT_EULA: Y
|
||||
# WARN: command overrides, default image start sequence, start.sh starts 'sql-server'
|
||||
command: '/var/setup/start.sh'
|
||||
volumes:
|
||||
- ./mssql:/var/setup # mount setup files
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
# see https://github.com/Microsoft/mssql-docker
|
||||
# no way to know when sql server is ready
|
||||
until /opt/mssql-tools/bin/sqlcmd -S 127.0.01 -U sa -P Pa55w0rd -d master -i /var/setup/setup.sql
|
||||
do sleep 1;
|
||||
done
|
||||
echo "NEXT_AUTH: setup completed"
|
||||
@@ -1,29 +0,0 @@
|
||||
USE master;
|
||||
/* did you tear down the container ? */
|
||||
if not exists (select name
|
||||
from sys.syslogins
|
||||
where name = 'nextauth')
|
||||
CREATE LOGIN nextauth
|
||||
WITH PASSWORD = 'password',
|
||||
CHECK_POLICY = OFF;
|
||||
GO
|
||||
/* did you tear down the container ? */
|
||||
if not exists (select name
|
||||
from sys.databases
|
||||
where name = 'nextauth' )
|
||||
CREATE database nextauth
|
||||
GO
|
||||
/* did you tear down the container ? */
|
||||
if not exists(select [name]
|
||||
from sys.sysusers
|
||||
where name= 'nextauth')
|
||||
CREATE USER nextauth
|
||||
WITH DEFAULT_SCHEMA =[dbo];
|
||||
GO
|
||||
/*
|
||||
* Adding user as sysadmin,
|
||||
* So you can easily drop/create/re-create/alter the database
|
||||
* You will need to login to 'master' to do that
|
||||
*/
|
||||
exec sp_addsrvrolemember @loginame = N'nextauth', @rolename = N'sysadmin'
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
# launch setup on the background & start server
|
||||
# otherise sqlservr won't start
|
||||
/var/setup/setup.sh & /opt/mssql/bin/sqlservr
|
||||
@@ -1,13 +0,0 @@
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
|
||||
mysql:
|
||||
image: mysql
|
||||
command: --default-authentication-plugin=mysql_native_password
|
||||
restart: always
|
||||
environment:
|
||||
MYSQL_USER: nextauth
|
||||
MYSQL_PASSWORD: password
|
||||
MYSQL_DATABASE: nextauth
|
||||
MYSQL_RANDOM_ROOT_PASSWORD: 'yes'
|
||||
@@ -1,11 +0,0 @@
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
|
||||
postgres:
|
||||
image: postgres
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_USER: nextauth
|
||||
POSTGRES_PASSWORD: password
|
||||
POSTGRES_DB: nextauth
|
||||
141
test/fixtures/schemas/mssql.json
vendored
141
test/fixtures/schemas/mssql.json
vendored
@@ -1,141 +0,0 @@
|
||||
{
|
||||
"users": {
|
||||
"id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"name": {
|
||||
"type": "varchar",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"email": {
|
||||
"type": "varchar",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"email_verified": {
|
||||
"type": "datetime",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"image": {
|
||||
"type": "varchar",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"created_at": {
|
||||
"type": "datetime",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "datetime",
|
||||
"nullable": false
|
||||
}
|
||||
},
|
||||
"accounts": {
|
||||
"id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"compound_id": {
|
||||
"type": "varchar",
|
||||
"nullable": false
|
||||
},
|
||||
"user_id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_type": {
|
||||
"type": "varchar",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_id": {
|
||||
"type": "varchar",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_account_id": {
|
||||
"type": "varchar",
|
||||
"nullable": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"type": "text",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"access_token": {
|
||||
"type": "text",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"access_token_expires": {
|
||||
"type": "datetime",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"created_at": {
|
||||
"type": "datetime",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "datetime",
|
||||
"nullable": false
|
||||
}
|
||||
},
|
||||
"sessions": {
|
||||
"id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"user_id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"expires": {
|
||||
"type": "datetime",
|
||||
"nullable": false
|
||||
},
|
||||
"session_token": {
|
||||
"type": "varchar",
|
||||
"nullable": false
|
||||
},
|
||||
"access_token": {
|
||||
"type": "varchar",
|
||||
"nullable": false
|
||||
},
|
||||
"created_at": {
|
||||
"type": "datetime",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "datetime",
|
||||
"nullable": false
|
||||
}
|
||||
},
|
||||
"verification_requests": {
|
||||
"id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"identifier": {
|
||||
"type": "varchar",
|
||||
"nullable": false
|
||||
},
|
||||
"token": {
|
||||
"type": "varchar",
|
||||
"nullable": false
|
||||
},
|
||||
"expires": {
|
||||
"type": "datetime",
|
||||
"nullable": false
|
||||
},
|
||||
"created_at": {
|
||||
"type": "datetime",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "datetime",
|
||||
"nullable": false
|
||||
}
|
||||
}
|
||||
}
|
||||
141
test/fixtures/schemas/mysql.json
vendored
141
test/fixtures/schemas/mysql.json
vendored
@@ -1,141 +0,0 @@
|
||||
{
|
||||
"users": {
|
||||
"id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"name": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"email": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"email_verified": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"image": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"created_at": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
}
|
||||
},
|
||||
"accounts": {
|
||||
"id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"compound_id": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": false
|
||||
},
|
||||
"user_id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_type": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_id": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_account_id": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"type": "text",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"access_token": {
|
||||
"type": "text",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"access_token_expires": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"created_at": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
}
|
||||
},
|
||||
"sessions": {
|
||||
"id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"user_id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"expires": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
},
|
||||
"session_token": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": false
|
||||
},
|
||||
"access_token": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": false
|
||||
},
|
||||
"created_at": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
}
|
||||
},
|
||||
"verification_requests": {
|
||||
"id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"identifier": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": false
|
||||
},
|
||||
"token": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": false
|
||||
},
|
||||
"expires": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
},
|
||||
"created_at": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
}
|
||||
}
|
||||
}
|
||||
141
test/fixtures/schemas/postgres.json
vendored
141
test/fixtures/schemas/postgres.json
vendored
@@ -1,141 +0,0 @@
|
||||
{
|
||||
"users": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"nullable": false
|
||||
},
|
||||
"name": {
|
||||
"type": "character varying",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"email": {
|
||||
"type": "character varying",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"email_verified": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"image": {
|
||||
"type": "character varying",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"created_at": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
}
|
||||
},
|
||||
"accounts": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"nullable": false
|
||||
},
|
||||
"compound_id": {
|
||||
"type": "character varying",
|
||||
"nullable": false
|
||||
},
|
||||
"user_id": {
|
||||
"type": "integer",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_type": {
|
||||
"type": "character varying",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_id": {
|
||||
"type": "character varying",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_account_id": {
|
||||
"type": "character varying",
|
||||
"nullable": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"type": "text",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"access_token": {
|
||||
"type": "text",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"access_token_expires": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"created_at": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
}
|
||||
},
|
||||
"sessions": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"nullable": false
|
||||
},
|
||||
"user_id": {
|
||||
"type": "integer",
|
||||
"nullable": false
|
||||
},
|
||||
"expires": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
},
|
||||
"session_token": {
|
||||
"type": "character varying",
|
||||
"nullable": false
|
||||
},
|
||||
"access_token": {
|
||||
"type": "character varying",
|
||||
"nullable": false
|
||||
},
|
||||
"created_at": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
}
|
||||
},
|
||||
"verification_requests": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"nullable": false
|
||||
},
|
||||
"identifier": {
|
||||
"type": "character varying",
|
||||
"nullable": false
|
||||
},
|
||||
"token": {
|
||||
"type": "character varying",
|
||||
"nullable": false
|
||||
},
|
||||
"expires": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
},
|
||||
"created_at": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
}
|
||||
}
|
||||
}
|
||||
47
test/fixtures/sql/mssql.sql
vendored
47
test/fixtures/sql/mssql.sql
vendored
@@ -1,47 +0,0 @@
|
||||
-- FIXME Missing indexes!
|
||||
CREATE TABLE accounts
|
||||
(
|
||||
id int IDENTITY(1,1) NOT NULL,
|
||||
compound_id varchar(255) NOT NULL,
|
||||
user_id int NOT NULL,
|
||||
provider_type varchar(255) NOT NULL,
|
||||
provider_id varchar(255) NOT NULL,
|
||||
provider_account_id varchar(255) NOT NULL,
|
||||
refresh_token text NULL,
|
||||
access_token text NULL,
|
||||
access_token_expires datetime NULL,
|
||||
created_at datetime NOT NULL DEFAULT getdate(),
|
||||
updated_at datetime NOT NULL DEFAULT getdate()
|
||||
);
|
||||
|
||||
CREATE TABLE sessions
|
||||
(
|
||||
id int IDENTITY(1,1) NOT NULL,
|
||||
user_id int NOT NULL,
|
||||
expires datetime NOT NULL,
|
||||
session_token varchar(255) NOT NULL,
|
||||
access_token varchar(255) NOT NULL,
|
||||
created_at datetime NOT NULL DEFAULT getdate(),
|
||||
updated_at datetime NOT NULL DEFAULT getdate()
|
||||
);
|
||||
|
||||
CREATE TABLE users
|
||||
(
|
||||
id int IDENTITY(1,1) NOT NULL,
|
||||
name varchar(255) NULL,
|
||||
email varchar(255) NULL,
|
||||
email_verified datetime NULL,
|
||||
image varchar(255) NULL,
|
||||
created_at datetime NOT NULL DEFAULT getdate(),
|
||||
updated_at datetime NOT NULL DEFAULT getdate()
|
||||
);
|
||||
|
||||
CREATE TABLE verification_requests
|
||||
(
|
||||
id int IDENTITY(1,1) NOT NULL,
|
||||
identifier varchar(255) NOT NULL,
|
||||
token varchar(255) NOT NULL,
|
||||
expires datetime NOT NULL,
|
||||
created_at datetime NOT NULL DEFAULT getdate(),
|
||||
updated_at datetime NOT NULL DEFAULT getdate()
|
||||
);
|
||||
74
test/fixtures/sql/mysql.sql
vendored
74
test/fixtures/sql/mysql.sql
vendored
@@ -1,74 +0,0 @@
|
||||
CREATE TABLE accounts
|
||||
(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
compound_id VARCHAR(255) NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
provider_type VARCHAR(255) NOT NULL,
|
||||
provider_id VARCHAR(255) NOT NULL,
|
||||
provider_account_id VARCHAR(255) NOT NULL,
|
||||
refresh_token TEXT,
|
||||
access_token TEXT,
|
||||
access_token_expires TIMESTAMP(6),
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE sessions
|
||||
(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
expires TIMESTAMP(6) NOT NULL,
|
||||
session_token VARCHAR(255) NOT NULL,
|
||||
access_token VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE users
|
||||
(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
name VARCHAR(255),
|
||||
email VARCHAR(255),
|
||||
email_verified TIMESTAMP(6),
|
||||
image VARCHAR(255),
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE verification_requests
|
||||
(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
identifier VARCHAR(255) NOT NULL,
|
||||
token VARCHAR(255) NOT NULL,
|
||||
expires TIMESTAMP(6) NOT NULL,
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX compound_id
|
||||
ON accounts(compound_id);
|
||||
|
||||
CREATE INDEX provider_account_id
|
||||
ON accounts(provider_account_id);
|
||||
|
||||
CREATE INDEX provider_id
|
||||
ON accounts(provider_id);
|
||||
|
||||
CREATE INDEX user_id
|
||||
ON accounts(user_id);
|
||||
|
||||
CREATE UNIQUE INDEX session_token
|
||||
ON sessions(session_token);
|
||||
|
||||
CREATE UNIQUE INDEX access_token
|
||||
ON sessions(access_token);
|
||||
|
||||
CREATE UNIQUE INDEX email
|
||||
ON users(email);
|
||||
|
||||
CREATE UNIQUE INDEX token
|
||||
ON verification_requests(token);
|
||||
74
test/fixtures/sql/postgres.sql
vendored
74
test/fixtures/sql/postgres.sql
vendored
@@ -1,74 +0,0 @@
|
||||
CREATE TABLE accounts
|
||||
(
|
||||
id SERIAL,
|
||||
compound_id VARCHAR(255) NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
provider_type VARCHAR(255) NOT NULL,
|
||||
provider_id VARCHAR(255) NOT NULL,
|
||||
provider_account_id VARCHAR(255) NOT NULL,
|
||||
refresh_token TEXT,
|
||||
access_token TEXT,
|
||||
access_token_expires TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE sessions
|
||||
(
|
||||
id SERIAL,
|
||||
user_id INTEGER NOT NULL,
|
||||
expires TIMESTAMPTZ NOT NULL,
|
||||
session_token VARCHAR(255) NOT NULL,
|
||||
access_token VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE users
|
||||
(
|
||||
id SERIAL,
|
||||
name VARCHAR(255),
|
||||
email VARCHAR(255),
|
||||
email_verified TIMESTAMPTZ,
|
||||
image VARCHAR(255),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE verification_requests
|
||||
(
|
||||
id SERIAL,
|
||||
identifier VARCHAR(255) NOT NULL,
|
||||
token VARCHAR(255) NOT NULL,
|
||||
expires TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX compound_id
|
||||
ON accounts(compound_id);
|
||||
|
||||
CREATE INDEX provider_account_id
|
||||
ON accounts(provider_account_id);
|
||||
|
||||
CREATE INDEX provider_id
|
||||
ON accounts(provider_id);
|
||||
|
||||
CREATE INDEX user_id
|
||||
ON accounts(user_id);
|
||||
|
||||
CREATE UNIQUE INDEX session_token
|
||||
ON sessions(session_token);
|
||||
|
||||
CREATE UNIQUE INDEX access_token
|
||||
ON sessions(access_token);
|
||||
|
||||
CREATE UNIQUE INDEX email
|
||||
ON users(email);
|
||||
|
||||
CREATE UNIQUE INDEX token
|
||||
ON verification_requests(token);
|
||||
@@ -1,71 +0,0 @@
|
||||
require('dotenv').config()
|
||||
const assert = require('assert')
|
||||
const { puppeteer, puppeteerOptions } = require('../lib/puppeteer')
|
||||
|
||||
const BASE_URL = 'http://localhost:3000'
|
||||
const CALLBACK_URL = `${BASE_URL}/test`
|
||||
|
||||
const {
|
||||
NEXTAUTH_GITHUB_USERNAME: USERNAME,
|
||||
NEXTAUTH_GITHUB_PASSWORD: PASSWORD
|
||||
} = process.env
|
||||
|
||||
describe('GitHub (OAuth 2.0 flow)', function () {
|
||||
this.slow(5000)
|
||||
this.timeout(1000 * 60)
|
||||
let browser,page
|
||||
|
||||
before(async () => {
|
||||
browser = await puppeteer.launch(puppeteerOptions)
|
||||
page = await browser.newPage()
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should show button on sign in page', async function () {
|
||||
page.setDefaultTimeout(1000 * 60)
|
||||
await page.goto(`${BASE_URL}/api/auth/signin?callbackUrl=${encodeURIComponent(CALLBACK_URL)}`)
|
||||
await page.waitForSelector(`form[action="${BASE_URL}/api/auth/signin/github"] button`)
|
||||
await page.click(`form[action="${BASE_URL}/api/auth/signin/github"] button`)
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be redirected to provider', async function () {
|
||||
// Enter username
|
||||
await page.waitForSelector('input[name="login"]')
|
||||
await page.click('input[name="login"]')
|
||||
await page.keyboard.type(USERNAME)
|
||||
|
||||
// Enter password
|
||||
await page.waitForSelector('input[name="password"]')
|
||||
await page.click('input[name="password"]')
|
||||
await page.keyboard.type(PASSWORD)
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be able to sign in with provider', async function () {
|
||||
// Click submit
|
||||
await page.click('form[action="/session"] [type="submit"]')
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be returned to callback URL', async function () {
|
||||
// Wait for page to return to callback URL
|
||||
await page.waitForSelector('#nextauth-test-page')
|
||||
|
||||
// Check we are at the correct callback URL
|
||||
assert.equal(page.url(), CALLBACK_URL)
|
||||
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be signed in', async function () {
|
||||
// Check we are signed in
|
||||
await page.waitForSelector('#nextauth-signed-in')
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await browser.close()
|
||||
return Promise.resolve()
|
||||
})
|
||||
})
|
||||
@@ -1,81 +0,0 @@
|
||||
require('dotenv').config()
|
||||
const assert = require('assert')
|
||||
const { puppeteer, puppeteerOptions } = require('../lib/puppeteer')
|
||||
|
||||
const BASE_URL = 'http://localhost:3000'
|
||||
const CALLBACK_URL = `${BASE_URL}/test`
|
||||
|
||||
const {
|
||||
NEXTAUTH_GOOGLE_USERNAME: USERNAME,
|
||||
NEXTAUTH_GOOGLE_PASSWORD: PASSWORD
|
||||
} = process.env
|
||||
|
||||
// This seems to stall because of a popup that is displayed only when using
|
||||
// puppeteer. See FIXME below. Would appreciate any help resolving it.
|
||||
describe.skip('Google (OAuth 2.0 flow)', function () {
|
||||
this.slow(5000)
|
||||
this.timeout(1000 * 60)
|
||||
let browser,page
|
||||
|
||||
before(async () => {
|
||||
browser = await puppeteer.launch(puppeteerOptions)
|
||||
page = await browser.newPage()
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should show button on sign in page', async function () {
|
||||
page.setDefaultTimeout(1000 * 60)
|
||||
await page.goto(`${BASE_URL}/api/auth/signin?callbackUrl=${encodeURIComponent(CALLBACK_URL)}`)
|
||||
await page.waitForSelector(`form[action="${BASE_URL}/api/auth/signin/google"] button`)
|
||||
await page.click(`form[action="${BASE_URL}/api/auth/signin/google"] button`)
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be redirected to provider', async function () {
|
||||
// Enter username
|
||||
await page.waitForSelector('input[type="email"]')
|
||||
await page.click('input[type="email"]')
|
||||
await page.keyboard.type(USERNAME)
|
||||
|
||||
// FIXME Work out how to dismiss popup
|
||||
// A popup *only* appears when using puppeteer (not manually) but I can't
|
||||
// get the xPath selectors to work to dismiss it. This is as close as I got.
|
||||
// await page.waitForXPath("(//span[contains(text(), 'Got it')])[2]")
|
||||
// const element = await page.$x("(//span[contains(text(), 'Got it')])[2]")
|
||||
// await element.click()
|
||||
|
||||
// Enter password
|
||||
await page.waitForSelector('input[type="password"]')
|
||||
await page.click('input[type="password"]')
|
||||
await page.keyboard.type(PASSWORD)
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be able to sign in with provider', async function () {
|
||||
// Click submit
|
||||
await page.click('button[type="button"]')
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be returned to callback URL', async function () {
|
||||
// Wait for page to return to callback URL
|
||||
await page.waitForSelector('#nextauth-test-page')
|
||||
|
||||
// Check we are at the correct callback URL
|
||||
// Note: Google OAuth appends a # to the end of the URL in Chrome
|
||||
assert.equal(page.url(), `${CALLBACK_URL}#`)
|
||||
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be signed in', async function () {
|
||||
// Check we are signed in
|
||||
await page.waitForSelector('#nextauth-signed-in')
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await browser.close()
|
||||
return Promise.resolve()
|
||||
})
|
||||
})
|
||||
@@ -1,71 +0,0 @@
|
||||
require('dotenv').config()
|
||||
const assert = require('assert')
|
||||
const { puppeteer, puppeteerOptions } = require('../lib/puppeteer')
|
||||
|
||||
const BASE_URL = 'http://localhost:3000'
|
||||
const CALLBACK_URL = `${BASE_URL}/test`
|
||||
|
||||
const {
|
||||
NEXTAUTH_TWITTER_USERNAME: USERNAME,
|
||||
NEXTAUTH_TWITTER_PASSWORD: PASSWORD,
|
||||
} = process.env
|
||||
|
||||
describe('Twitter (OAuth 1.1 flow)', async function () {
|
||||
this.slow(5000)
|
||||
this.timeout(1000 * 60)
|
||||
let browser,page
|
||||
|
||||
before(async () => {
|
||||
browser = await puppeteer.launch(puppeteerOptions)
|
||||
page = await browser.newPage()
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should show button on sign in page', async function () {
|
||||
page.setDefaultTimeout(1000 * 60)
|
||||
await page.goto(`${BASE_URL}/api/auth/signin?callbackUrl=${encodeURIComponent(CALLBACK_URL)}`)
|
||||
await page.waitForSelector(`form[action="${BASE_URL}/api/auth/signin/twitter"] button`)
|
||||
await page.click(`form[action="${BASE_URL}/api/auth/signin/twitter"] button`)
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be redirected to provider', async function () {
|
||||
// Enter username
|
||||
await page.waitForSelector('input[name="session[username_or_email]"]')
|
||||
await page.click('input[name="session[username_or_email]"]')
|
||||
await page.keyboard.type(USERNAME)
|
||||
|
||||
// Enter password
|
||||
await page.waitForSelector('input[name="session[password]"]')
|
||||
await page.click('input[name="session[password]"]')
|
||||
await page.keyboard.type(PASSWORD)
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be able to sign in with provider', async function () {
|
||||
// Click submit
|
||||
await page.click('form[action="https://api.twitter.com/oauth/authenticate"] [type="submit"]')
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be returned to callback URL', async function () {
|
||||
// Wait for page to return to callback URL
|
||||
await page.waitForSelector('#nextauth-test-page')
|
||||
|
||||
// Check we are at the correct callback URL
|
||||
assert.equal(page.url(), CALLBACK_URL)
|
||||
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be signed in', async function () {
|
||||
// Check we are signed in
|
||||
await page.waitForSelector('#nextauth-signed-in')
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await browser.close()
|
||||
return Promise.resolve()
|
||||
})
|
||||
})
|
||||
@@ -1,36 +0,0 @@
|
||||
exports.compareSchemas = (expected, actual) => {
|
||||
const errors = []
|
||||
|
||||
// Check all models and properties that are expected exist
|
||||
for (const objectName in expected) {
|
||||
if (actual[objectName]) {
|
||||
for (const propertyName in expected[objectName]) {
|
||||
if (actual[objectName][propertyName]) {
|
||||
if (JSON.stringify(expected[objectName][propertyName]) !== JSON.stringify(actual[objectName][propertyName])) {
|
||||
errors.push(`${objectName}.${propertyName} does not match expected result`)
|
||||
}
|
||||
} else {
|
||||
errors.push(`${objectName}.${propertyName} not found (should exist)`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
errors.push(`${objectName} not found (should exist)`)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for models and properties that exist but are not expected
|
||||
for (const objectName in actual) {
|
||||
if (expected[objectName]) {
|
||||
for (const propertyName in actual[objectName]) {
|
||||
if (!expected[objectName][propertyName]) {
|
||||
errors.push(`${objectName}.${propertyName} found (not expected)`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
errors.push(`${objectName} found (not expected)`)
|
||||
}
|
||||
}
|
||||
|
||||
// Return true if no errors, else return array of errors
|
||||
return errors.length > 0 ? errors : true
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
const puppeteerExtra = require('puppeteer-extra')
|
||||
const StealthPlugin = require("puppeteer-extra-plugin-stealth")
|
||||
const stealthPlugin = StealthPlugin()
|
||||
|
||||
// Override Puppeteer user agent to set 'navigator.platform' explicitly to
|
||||
// prevent detection on some providers (e.g. GitHub OAuth) as they force 2FA
|
||||
// on sign in if they detect sign in from a platform they haven't seen before.
|
||||
const puppeteerExtraPluginUserAgentOverride = require("puppeteer-extra-plugin-stealth/evasions/user-agent-override")
|
||||
stealthPlugin.enabledEvasions.delete("user-agent-override")
|
||||
puppeteerExtra.use(stealthPlugin)
|
||||
const pluginUserAgentOverride = puppeteerExtraPluginUserAgentOverride({
|
||||
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36",
|
||||
platform: "MacIntel"
|
||||
})
|
||||
puppeteerExtra.use(pluginUserAgentOverride)
|
||||
|
||||
// CI is set to true by GitHub Actions to indicate is running in CD/CI
|
||||
const { CI } = process.env
|
||||
|
||||
const puppeteerOptions = {
|
||||
headless: true // Set to 'false' to debug more easily
|
||||
}
|
||||
|
||||
// When running on remote test runner (which is ARM) the executable path
|
||||
// needs to be set to 'chromium-browser' so it uses the ARM build of Chromium
|
||||
// not the x86 build that Puppeteer uses by default. Supporting this allows us
|
||||
// to test easily from remote locations that are outside cloud networks like
|
||||
// AWS, GPC, Azure, etc. and avoids tests being thwarted by IP blocklists.
|
||||
if (CI)
|
||||
puppeteerOptions.executablePath = 'chromium-browser'
|
||||
|
||||
module.exports = {
|
||||
puppeteer: puppeteerExtra,
|
||||
puppeteerOptions
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/* eslint-disable */
|
||||
// Placeholder for schema test (will use test framework, this is temporary)
|
||||
const Adapters = require('../adapters')
|
||||
|
||||
;(async () => {
|
||||
try {
|
||||
// We can't connection a local MongoDB SRV instance but we can at least see if the URLs cause an error
|
||||
Adapters.Default('mongodb+srv://nextauth:password@127.0.0.1/nextauth?ssl=false&retryWrites=true')
|
||||
|
||||
// Connect to local MongoDB instance
|
||||
// Note: MongoDB doesn't thrown a connection error right away if is a
|
||||
// problem with the credentials or host configuration, but after a few
|
||||
// seconds it throws a Timeout error (which is caught by the adapter).
|
||||
const adapter = Adapters.Default('mongodb://nextauth:password@127.0.0.1:27017/nextauth?synchronize=true')
|
||||
await adapter.getAdapter()
|
||||
|
||||
// @TODO create objects in database, check format of objects returned
|
||||
|
||||
console.log('MongoDB loaded ok')
|
||||
process.exit()
|
||||
} catch (error) {
|
||||
console.error('MongoDB error', error)
|
||||
process.exit(1)
|
||||
}
|
||||
})()
|
||||
106
test/mssql.js
106
test/mssql.js
@@ -1,106 +0,0 @@
|
||||
/* eslint-disable */
|
||||
// Placeholder for schema test (will use test framework, this is temporary)
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const mssql = require('mssql');
|
||||
const Adapters = require('../adapters');
|
||||
|
||||
const SCHEMA_FILE = path.join(__dirname, '/fixtures/schemas/mssql.json');
|
||||
const expectedSchema = JSON.parse(fs.readFileSync(SCHEMA_FILE));
|
||||
const TABLES = Object.keys(expectedSchema);
|
||||
const databaseUrl = `mssql://nextauth:password@127.0.0.1:1433/nextauth?synchronize=true`;
|
||||
|
||||
function printSchema() {
|
||||
return new Promise(async (resolve) => {
|
||||
/**
|
||||
* @type {import('mssql').ConnectionPool}
|
||||
*/
|
||||
let connection;
|
||||
try {
|
||||
connection = await mssql.connect(databaseUrl);
|
||||
// Invoke adapter to sync schema
|
||||
await (Adapters.Default(databaseUrl)).getAdapter();
|
||||
// query schema
|
||||
const { recordset } = await connection.query(
|
||||
`use [nextauth]; ` +
|
||||
TABLES.map(
|
||||
(table) =>
|
||||
`select * from INFORMATION_SCHEMA.COLUMNS` +
|
||||
` where TABLE_NAME = '${table}'`
|
||||
).join(' UNION ALL ')
|
||||
);
|
||||
// build result
|
||||
return resolve(
|
||||
TABLES.reduce(
|
||||
(out, next) => ({
|
||||
...out,
|
||||
[next]: collect(recordset, next),
|
||||
}),
|
||||
{}
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
} finally {
|
||||
if (connection) {
|
||||
connection.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
const assert = require('assert');
|
||||
/** RUN */
|
||||
(async () => {
|
||||
try {
|
||||
const testResultSchema = await printSchema();
|
||||
const actualTables = Object.keys(testResultSchema);
|
||||
assert.equal(
|
||||
TABLES,
|
||||
actualTables.join(),
|
||||
`MSSQL Schema: Expected tables [${TABLES.join()}]\n to be [${actualTables.join()}]`
|
||||
);
|
||||
//cheap deepEquals, with hints
|
||||
for (const tableName of TABLES) {
|
||||
const newLocal = expectedSchema[tableName];
|
||||
for (const columnName of Object.keys(newLocal)) {
|
||||
const expected = expectedSchema[tableName][columnName];
|
||||
const actual = testResultSchema[tableName][columnName];
|
||||
for (const propKey of Object.keys(expected)) {
|
||||
assert.equal(
|
||||
expected[propKey],
|
||||
actual[propKey],
|
||||
`Expected ${tableName}.${columnName}.${propKey}=${actual[propKey]}` +
|
||||
` to be ${expected[propKey]}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('mssql: schema ok');
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
})()
|
||||
.then(() => process.exit())
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(-1);
|
||||
});
|
||||
/** collect results */
|
||||
const collect = (records, tableName) => {
|
||||
const keys = Object.keys(expectedSchema[tableName]);
|
||||
const ret = records
|
||||
.filter((x) => x.TABLE_NAME === tableName)
|
||||
.reduce((out, x) => {
|
||||
if (keys.indexOf(x.COLUMN_NAME) === -1) return out; //map only required columns
|
||||
const nullable = x.IS_NULLABLE === 'YES';
|
||||
return {
|
||||
...out,
|
||||
[x.COLUMN_NAME]: {
|
||||
nullable,
|
||||
type: x.DATA_TYPE,
|
||||
default: (nullable && x.COLUMN_DEFAULT) || undefined,
|
||||
},
|
||||
};
|
||||
}, {});
|
||||
return ret;
|
||||
};
|
||||
@@ -1,74 +0,0 @@
|
||||
/* eslint-disable */
|
||||
// Placeholder for schema test (will use test framework, this is temporary)
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const mysql = require('mysql')
|
||||
|
||||
const { compareSchemas } = require('./lib/db')
|
||||
const Adapters = require('../adapters')
|
||||
|
||||
const TABLES = ['users', 'accounts', 'sessions', 'verification_requests']
|
||||
const SCHEMA_FILE = path.join(__dirname, '/fixtures/schemas/mysql.json')
|
||||
|
||||
function printSchema () {
|
||||
return new Promise(async (resolve) => {
|
||||
// Invoke adapter to sync schema
|
||||
const adapter = Adapters.Default('mysql://nextauth:password@127.0.0.1:3306/nextauth?synchronize=true')
|
||||
await adapter.getAdapter()
|
||||
|
||||
const connection = mysql.createConnection({
|
||||
host: '127.0.0.1',
|
||||
user: 'nextauth',
|
||||
password: 'password',
|
||||
database: 'nextauth',
|
||||
port: 3306,
|
||||
multipleStatements: true
|
||||
})
|
||||
|
||||
connection.connect()
|
||||
connection.query(
|
||||
TABLES.map(table => `DESCRIBE ${table}`).join(';'),
|
||||
(error, result) => {
|
||||
if (error) { throw error }
|
||||
|
||||
const getColumnSchema = (column) => {
|
||||
const nullable = column.Null === 'YES' ? true : false
|
||||
return {
|
||||
type: column.Type,
|
||||
nullable,
|
||||
default: nullable ? column.Default : undefined
|
||||
}
|
||||
}
|
||||
|
||||
const users = {}
|
||||
const accounts = {}
|
||||
const sessions = {}
|
||||
const verification_requests = {}
|
||||
|
||||
result[0].forEach(column => { users[column.Field] = getColumnSchema(column) })
|
||||
result[1].forEach(column => { accounts[column.Field] = getColumnSchema(column) })
|
||||
result[2].forEach(column => { sessions[column.Field] = getColumnSchema(column) })
|
||||
result[3].forEach(column => { verification_requests[column.Field] = getColumnSchema(column) })
|
||||
|
||||
connection.end()
|
||||
|
||||
resolve({ users, accounts, sessions, verification_requests })
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const expectedSchema = JSON.parse(fs.readFileSync(SCHEMA_FILE))
|
||||
const testResultSchema = await printSchema()
|
||||
const compareResult = compareSchemas(expectedSchema, testResultSchema)
|
||||
if (compareResult === true) {
|
||||
console.log('MySQL schema ok')
|
||||
process.exit()
|
||||
} else {
|
||||
console.error('MySQL schema errors')
|
||||
compareResult.forEach(error => console.log(` * ${error}`))
|
||||
console.log('MySQL schema found:', JSON.stringify(testResultSchema, null, 2))
|
||||
process.exit(1)
|
||||
}
|
||||
})()
|
||||
@@ -1,73 +0,0 @@
|
||||
/* eslint-disable */
|
||||
// Placeholder for schema test (will use test framework, this is temporary)
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { Client } = require('pg')
|
||||
|
||||
const { compareSchemas } = require('./lib/db')
|
||||
const Adapters = require('../adapters')
|
||||
|
||||
const TABLES = ['users', 'accounts', 'sessions', 'verification_requests']
|
||||
const SCHEMA_FILE = path.join(__dirname, '/fixtures/schemas/postgres.json')
|
||||
|
||||
function printSchema () {
|
||||
return new Promise(async (resolve) => {
|
||||
// Invoke adapter to sync schema
|
||||
const adapter = Adapters.Default('postgres://nextauth:password@127.0.0.1:5432/nextauth?synchronize=true')
|
||||
await adapter.getAdapter()
|
||||
|
||||
const connection = new Client({
|
||||
host: '127.0.0.1',
|
||||
user: 'nextauth',
|
||||
password: 'password',
|
||||
database: 'nextauth',
|
||||
port: 5432
|
||||
})
|
||||
|
||||
connection.connect()
|
||||
connection.query(
|
||||
TABLES.map(table => `SELECT column_name, data_type, character_maximum_length, is_nullable, column_default FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = '${table}' ORDER BY ordinal_position`).join(';'),
|
||||
(error, result) => {
|
||||
if (error) { throw error }
|
||||
|
||||
const getColumnSchema = (column) => {
|
||||
const nullable = column.is_nullable === 'YES' ? true : false
|
||||
return {
|
||||
type: column.data_type,
|
||||
nullable,
|
||||
default: nullable ? column.column_default : undefined
|
||||
}
|
||||
}
|
||||
|
||||
const users = {}
|
||||
const accounts = {}
|
||||
const sessions = {}
|
||||
const verification_requests = {}
|
||||
|
||||
result[0].rows.forEach(column => { users[column.column_name] = getColumnSchema(column) })
|
||||
result[1].rows.forEach(column => { accounts[column.column_name] = getColumnSchema(column) })
|
||||
result[2].rows.forEach(column => { sessions[column.column_name] = getColumnSchema(column) })
|
||||
result[3].rows.forEach(column => { verification_requests[column.column_name] = getColumnSchema(column) })
|
||||
|
||||
connection.end()
|
||||
|
||||
resolve({ users, accounts, sessions, verification_requests })
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const expectedSchema = JSON.parse(fs.readFileSync(SCHEMA_FILE))
|
||||
const testResultSchema = await printSchema()
|
||||
const compareResult = compareSchemas(expectedSchema, testResultSchema)
|
||||
if (compareResult === true) {
|
||||
console.log('Postgres schema ok')
|
||||
process.exit()
|
||||
} else {
|
||||
console.error('Postgres schema errors')
|
||||
compareResult.forEach(error => console.log(` * ${error}`))
|
||||
console.log('Postgres schema found:', JSON.stringify(testResultSchema, null, 2))
|
||||
process.exit(1)
|
||||
}
|
||||
})()
|
||||
347
types/adapters.d.ts
vendored
347
types/adapters.d.ts
vendored
@@ -1,244 +1,131 @@
|
||||
import { AppOptions } from "./internals"
|
||||
import { ConnectionOptions, EntitySchema } from "typeorm"
|
||||
import { User } from "."
|
||||
import { AppProvider } from "./providers"
|
||||
import { User, Profile, Session } from "."
|
||||
import { EmailConfig, SendVerificationRequest } from "./providers"
|
||||
import { ConnectionOptions } from "typeorm"
|
||||
|
||||
export interface Profile {
|
||||
id: string
|
||||
name: string
|
||||
email: string | null
|
||||
image?: string | null
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
userId: string | number | object
|
||||
expires: Date
|
||||
sessionToken: string
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
export interface VerificationRequest {
|
||||
identifier: string
|
||||
token: string
|
||||
expires: Date
|
||||
}
|
||||
|
||||
export interface SendVerificationRequestParams {
|
||||
identifier: string
|
||||
url: string
|
||||
token: string
|
||||
baseUrl: string
|
||||
provider: AppProvider
|
||||
}
|
||||
|
||||
export type EmailAppProvider = AppProvider & {
|
||||
sendVerificationRequest: (
|
||||
params: SendVerificationRequestParams
|
||||
) => Promise<void>
|
||||
maxAge: number | undefined
|
||||
}
|
||||
|
||||
export interface AdapterInstance<
|
||||
TUser,
|
||||
TProfile,
|
||||
TSession,
|
||||
TVerificationRequest
|
||||
> {
|
||||
createUser: (profile: TProfile) => Promise<TUser>
|
||||
getUser: (id: string) => Promise<TUser | null>
|
||||
getUserByEmail: (email: string) => Promise<TUser | null>
|
||||
getUserByProviderAccountId: (
|
||||
providerId: string,
|
||||
providerAccountId: string
|
||||
) => Promise<TUser | null>
|
||||
updateUser: (user: TUser) => Promise<TUser>
|
||||
linkAccount: (
|
||||
userId: string,
|
||||
providerId: string,
|
||||
providerType: string,
|
||||
providerAccountId: string,
|
||||
refreshToken: string,
|
||||
accessToken: string,
|
||||
accessTokenExpires: number
|
||||
) => Promise<void>
|
||||
createSession: (user: TUser) => Promise<TSession>
|
||||
getSession: (sessionToken: string) => Promise<TSession | null>
|
||||
updateSession: (session: TSession, force?: boolean) => Promise<TSession>
|
||||
deleteSession: (sessionToken: string) => Promise<void>
|
||||
createVerificationRequest?: (
|
||||
email: string,
|
||||
url: string,
|
||||
token: string,
|
||||
secret: string,
|
||||
provider: EmailAppProvider,
|
||||
options: AppOptions
|
||||
) => Promise<TVerificationRequest>
|
||||
getVerificationRequest?: (
|
||||
email: string,
|
||||
verificationToken: string,
|
||||
secret: string,
|
||||
provider: AppProvider
|
||||
) => Promise<TVerificationRequest | null>
|
||||
deleteVerificationRequest?: (
|
||||
email: string,
|
||||
verificationToken: string,
|
||||
secret: string,
|
||||
provider: AppProvider
|
||||
) => Promise<void>
|
||||
}
|
||||
|
||||
interface Adapter<
|
||||
TUser extends User = any,
|
||||
TProfile extends Profile = any,
|
||||
TSession extends Session = any,
|
||||
TVerificationRequest extends VerificationRequest = any
|
||||
> {
|
||||
getAdapter: (
|
||||
appOptions: AppOptions
|
||||
) => Promise<AdapterInstance<TUser, TProfile, TSession, TVerificationRequest>>
|
||||
}
|
||||
|
||||
type Schema<T = any> = EntitySchema<T>["options"]
|
||||
|
||||
interface BuiltInAdapters {
|
||||
Default: TypeORMAdapter["Adapter"]
|
||||
TypeORM: TypeORMAdapter
|
||||
Prisma: PrismaAdapter
|
||||
/** Legacy */
|
||||
declare const Adapters: {
|
||||
Default: Adapter<ConnectionOptions>
|
||||
TypeORM: { Adapter: Adapter<ConnectionOptions> }
|
||||
Prisma: { Adapter: Adapter }
|
||||
}
|
||||
export default Adapters
|
||||
|
||||
/**
|
||||
* TODO: fix auto-type schema
|
||||
* Using a custom adapter you can connect to any database backend or even several different databases.
|
||||
* Custom adapters created and maintained by our community can be found in the adapters repository.
|
||||
* Feel free to add a custom adapter from your project to the repository,
|
||||
* or even become a maintainer of a certain adapter.
|
||||
* Custom adapters can still be created and used in a project without being added to the repository.
|
||||
*
|
||||
* [Community adapters](https://github.com/nextauthjs/adapters) |
|
||||
* [Create a custom adapter](https://next-auth.js.org/tutorials/creating-a-database-adapter)
|
||||
*/
|
||||
|
||||
interface TypeORMAdapter<
|
||||
A extends TypeORMAccountModel = any,
|
||||
U extends TypeORMUserModel = any,
|
||||
S extends TypeORMSessionModel = any,
|
||||
VR extends TypeORMVerificationRequestModel = any
|
||||
> {
|
||||
Adapter: (
|
||||
typeOrmConfig: ConnectionOptions,
|
||||
options?: {
|
||||
models?: {
|
||||
Account?: {
|
||||
model: A
|
||||
schema: Schema<A>
|
||||
}
|
||||
User?: {
|
||||
model: U
|
||||
schema: Schema<U>
|
||||
}
|
||||
Session?: {
|
||||
model: S
|
||||
schema: Schema<S>
|
||||
}
|
||||
VerificationRequest?: {
|
||||
model: VR
|
||||
schema: Schema<VR>
|
||||
}
|
||||
}
|
||||
}
|
||||
) => Adapter<U, Profile, S, VR>
|
||||
Models: {
|
||||
Account: {
|
||||
model: TypeORMAccountModel
|
||||
schema: Schema<TypeORMAccountModel>
|
||||
}
|
||||
User: {
|
||||
model: TypeORMUserModel
|
||||
schema: Schema<TypeORMUserModel>
|
||||
}
|
||||
Session: {
|
||||
model: TypeORMSessionModel
|
||||
schema: Schema<TypeORMSessionModel>
|
||||
}
|
||||
VerificationRequest: {
|
||||
model: TypeORMVerificationRequestModel
|
||||
schema: Schema<TypeORMVerificationRequestModel>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface PrismaAdapter {
|
||||
Adapter: (config: {
|
||||
prisma: any
|
||||
modelMapping?: {
|
||||
User: string
|
||||
Account: string
|
||||
Session: string
|
||||
VerificationRequest: string
|
||||
}
|
||||
}) => Adapter
|
||||
}
|
||||
|
||||
declare class TypeORMAccountModel {
|
||||
compoundId: string
|
||||
userId: number
|
||||
providerType: string
|
||||
providerId: string
|
||||
providerAccountId: string
|
||||
refreshToken?: string
|
||||
accessToken?: string
|
||||
accessTokenExpires?: Date
|
||||
|
||||
constructor(
|
||||
userId: number,
|
||||
export interface AdapterInstance<U = User, P = Profile, S = Session> {
|
||||
createUser(profile: P): Promise<U>
|
||||
getUser(id: string): Promise<U | null>
|
||||
getUserByEmail(email: string): Promise<U | null>
|
||||
getUserByProviderAccountId(
|
||||
providerId: string,
|
||||
providerAccountId: string
|
||||
): Promise<U | null>
|
||||
updateUser(user: U): Promise<U>
|
||||
/** @todo Implement */
|
||||
deleteUser?(userId: string): Promise<void>
|
||||
linkAccount(
|
||||
userId: string,
|
||||
providerId: string,
|
||||
providerType: string,
|
||||
providerAccountId: string,
|
||||
refreshToken?: string,
|
||||
accessToken?: string,
|
||||
accessTokenExpires?: Date
|
||||
)
|
||||
accessTokenExpires?: null
|
||||
): Promise<void>
|
||||
/** @todo Implement */
|
||||
unlinkAccount?(
|
||||
userId: string,
|
||||
providerId: string,
|
||||
providerAccountId: string
|
||||
): Promise<void>
|
||||
createSession(user: U): Promise<S>
|
||||
getSession(sessionToken: string): Promise<S | null>
|
||||
updateSession(session: S, force?: boolean): Promise<S | null>
|
||||
deleteSession(sessionToken: string): Promise<void>
|
||||
createVerificationRequest?(
|
||||
identifier: string,
|
||||
url: string,
|
||||
token: string,
|
||||
secret: string,
|
||||
provider: EmailConfig & { maxAge: number; from: string }
|
||||
): Promise<void>
|
||||
getVerificationRequest?(
|
||||
identifier: string,
|
||||
verificationToken: string,
|
||||
secret: string,
|
||||
provider: Required<EmailConfig>
|
||||
): Promise<{
|
||||
id: string
|
||||
identifier: string
|
||||
token: string
|
||||
expires: Date
|
||||
} | null>
|
||||
deleteVerificationRequest?(
|
||||
identifier: string,
|
||||
verificationToken: string,
|
||||
secret: string,
|
||||
provider: Required<EmailConfig>
|
||||
): Promise<void>
|
||||
}
|
||||
|
||||
declare class TypeORMUserModel implements User {
|
||||
name?: string
|
||||
email?: string
|
||||
image?: string
|
||||
emailVerified?: Date
|
||||
|
||||
constructor(
|
||||
name?: string,
|
||||
email?: string,
|
||||
image?: string,
|
||||
emailVerified?: Date
|
||||
)
|
||||
}
|
||||
|
||||
declare class TypeORMSessionModel implements Session {
|
||||
userId: number
|
||||
expires: Date
|
||||
sessionToken: string
|
||||
accessToken: string
|
||||
|
||||
constructor(
|
||||
userId: number,
|
||||
expires: Date,
|
||||
sessionToken?: string,
|
||||
accessToken?: string
|
||||
)
|
||||
}
|
||||
|
||||
declare class TypeORMVerificationRequestModel implements VerificationRequest {
|
||||
identifier: string
|
||||
token: string
|
||||
expires: Date
|
||||
|
||||
constructor(identifier: string, token: string, expires: Date)
|
||||
}
|
||||
|
||||
declare const Adapters: BuiltInAdapters
|
||||
|
||||
export default Adapters
|
||||
|
||||
export {
|
||||
Adapter,
|
||||
BuiltInAdapters as Adapters,
|
||||
TypeORMAdapter,
|
||||
TypeORMAccountModel,
|
||||
TypeORMUserModel,
|
||||
TypeORMSessionModel,
|
||||
TypeORMVerificationRequestModel,
|
||||
PrismaAdapter,
|
||||
/**
|
||||
* From an implementation perspective, an adapter in NextAuth.js is a function
|
||||
* which returns an async `getAdapter()` method, which in turn returns a list of functions
|
||||
* used to handle operations such as creating user, linking a user
|
||||
* and an OAuth account or handling reading and writing sessions.
|
||||
*
|
||||
* It uses this approach to allow database connection logic to live in the `getAdapter()` method.
|
||||
* By calling the function just before an action needs to happen,
|
||||
* it is possible to check database connection status and handle connecting / reconnecting
|
||||
* to a database as required.
|
||||
*
|
||||
* **Required methods**
|
||||
*
|
||||
* _(These methods are required for all sign in flows)_
|
||||
* - `createUser`
|
||||
* - `getUser`
|
||||
* - `getUserByEmail`
|
||||
* - `getUserByProviderAccountId`
|
||||
* - `linkAccount`
|
||||
* - `createSession`
|
||||
* - `getSession`
|
||||
* - `updateSession`
|
||||
* - `deleteSession`
|
||||
* - `updateUser`
|
||||
*
|
||||
* _(Required to support email / passwordless sign in)_
|
||||
*
|
||||
* - `createVerificationRequest`
|
||||
* - `getVerificationRequest`
|
||||
* - `deleteVerificationRequest`
|
||||
*
|
||||
* **Unimplemented methods**
|
||||
*
|
||||
* _(These methods will be required in a future release, but are not yet invoked)_
|
||||
* - `deleteUser`
|
||||
* - `unlinkAccount`
|
||||
*
|
||||
* [Community adapters](https://github.com/nextauthjs/adapters) |
|
||||
* [Create a custom adapter](https://next-auth.js.org/tutorials/creating-a-database-adapter)
|
||||
*/
|
||||
export type Adapter<
|
||||
C = unknown,
|
||||
O = Record<string, unknown>,
|
||||
U = unknown,
|
||||
P = unknown,
|
||||
S = unknown
|
||||
> = (
|
||||
client: C,
|
||||
options?: O
|
||||
) => {
|
||||
getAdapter(appOptions: AppOptions): Promise<AdapterInstance<U, P, S>>
|
||||
}
|
||||
|
||||
4
types/client.d.ts
vendored
4
types/client.d.ts
vendored
@@ -32,7 +32,7 @@ export function useSession(): [Session | null, boolean]
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/getting-started/client#getsession)
|
||||
*/
|
||||
export function getSession(options: GetSessionOptions): Promise<Session | null>
|
||||
export function getSession(options?: GetSessionOptions): Promise<Session | null>
|
||||
|
||||
/**
|
||||
* Alias for `getSession`
|
||||
@@ -52,7 +52,7 @@ export const session: typeof getSession
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/getting-started/client#getcsrftoken)
|
||||
*/
|
||||
export function getCsrfToken(ctxOrReq: CtxOrReq): Promise<string | null>
|
||||
export function getCsrfToken(ctxOrReq?: CtxOrReq): Promise<string | null>
|
||||
|
||||
/**
|
||||
* Alias for `getCsrfToken`
|
||||
|
||||
23
types/errors.d.ts
vendored
Normal file
23
types/errors.d.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Same as the default `Error`, but it is JSON serializable.
|
||||
* @source https://iaincollins.medium.com/error-handling-in-javascript-a6172ccdf9af
|
||||
*/
|
||||
export class UnknownError extends Error {}
|
||||
export class OAuthCallbackError extends UnknownError {}
|
||||
export class AccountNotLinkedError extends UnknownError {}
|
||||
export class CreateUserError extends UnknownError {}
|
||||
export class GetUserError extends UnknownError {}
|
||||
export class GetUserByEmailError extends UnknownError {}
|
||||
export class GetUserByIdError extends UnknownError {}
|
||||
export class GetUserByProviderAccountIdError extends UnknownError {}
|
||||
export class UpdateUserError extends UnknownError {}
|
||||
export class DeleteUserError extends UnknownError {}
|
||||
export class LinkAccountError extends UnknownError {}
|
||||
export class UnlinkAccountError extends UnknownError {}
|
||||
export class CreateSessionError extends UnknownError {}
|
||||
export class GetSessionError extends UnknownError {}
|
||||
export class UpdateSessionError extends UnknownError {}
|
||||
export class DeleteSessionError extends UnknownError {}
|
||||
export class CreateVerificationRequestError extends UnknownError {}
|
||||
export class GetVerificationRequestError extends UnknownError {}
|
||||
export class DeleteVerificationRequestError extends UnknownError {}
|
||||
32
types/index.d.ts
vendored
32
types/index.d.ts
vendored
@@ -127,7 +127,7 @@ export interface NextAuthOptions {
|
||||
* [Default adapter](https://next-auth.js.org/schemas/adapters#typeorm-adapter) |
|
||||
* [Community adapters](https://github.com/nextauthjs/adapters)
|
||||
*/
|
||||
adapter?: Adapter
|
||||
adapter?: ReturnType<Adapter>
|
||||
/**
|
||||
* Set debug to true to enable debug messages for authentication and database operations.
|
||||
* * **Default value**: `false`
|
||||
@@ -180,7 +180,7 @@ export interface NextAuthOptions {
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/configuration/options#theme) | [Pages documentation]("https://next-auth.js.org/configuration/pages")
|
||||
*/
|
||||
theme?: "auto" | "dark" | "light"
|
||||
theme?: Theme
|
||||
/**
|
||||
* When set to `true` then all cookies set by NextAuth.js will only be accessible from HTTPS URLs.
|
||||
* This option defaults to `false` on URLs that start with `http://` (e.g. http://localhost:3000) for developer convenience.
|
||||
@@ -215,6 +215,14 @@ export interface NextAuthOptions {
|
||||
cookies?: CookiesOptions
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the theme of the built-in pages.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/configuration/options#theme) |
|
||||
* [Pages](https://next-auth.js.org/configuration/pages)
|
||||
*/
|
||||
export type Theme = "auto" | "dark" | "light"
|
||||
|
||||
/**
|
||||
* Override any of the methods, and the rest will use the default logger.
|
||||
*
|
||||
@@ -233,6 +241,8 @@ export interface LoggerInstance {
|
||||
*/
|
||||
export interface TokenSet {
|
||||
accessToken: string
|
||||
/** Kept for historical reasons, check out `expires_in` */
|
||||
accessTokenExpires: null
|
||||
idToken?: string
|
||||
refreshToken?: string
|
||||
access_token: string
|
||||
@@ -251,14 +261,16 @@ export interface Account extends TokenSet, Record<string, unknown> {
|
||||
type: string
|
||||
}
|
||||
|
||||
/** The OAuth profile returned from your provider */
|
||||
export interface Profile extends Record<string, unknown> {
|
||||
export interface DefaultProfile {
|
||||
sub?: string
|
||||
name?: string
|
||||
email?: string
|
||||
image?: string
|
||||
}
|
||||
|
||||
/** The OAuth profile returned from your provider */
|
||||
export interface Profile extends Record<string, unknown>, DefaultProfile {}
|
||||
|
||||
/** [Documentation](https://next-auth.js.org/configuration/callbacks) */
|
||||
export interface CallbacksOptions<
|
||||
P extends Record<string, unknown> = Profile,
|
||||
@@ -391,6 +403,12 @@ export interface SessionOptions {
|
||||
updateAge?: number
|
||||
}
|
||||
|
||||
export interface DefaultUser {
|
||||
name?: string | null
|
||||
email?: string | null
|
||||
image?: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* The shape of the returned object in the OAuth providers' `profile` callback,
|
||||
* available in the `jwt` and `session` callbacks,
|
||||
@@ -401,11 +419,7 @@ export interface SessionOptions {
|
||||
* [`jwt` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
|
||||
* [`profile` OAuth provider callback](https://next-auth.js.org/configuration/providers#using-a-custom-provider)
|
||||
*/
|
||||
export interface User {
|
||||
name?: string | null
|
||||
email?: string | null
|
||||
image?: string | null
|
||||
}
|
||||
export interface User extends Record<string, unknown>, DefaultUser {}
|
||||
|
||||
declare function NextAuth(
|
||||
req: NextApiRequest,
|
||||
|
||||
37
types/internals/index.d.ts
vendored
37
types/internals/index.d.ts
vendored
@@ -1,5 +1,5 @@
|
||||
import { NextApiRequest, NextApiResponse } from "./utils"
|
||||
import { NextAuthOptions } from ".."
|
||||
import { LoggerInstance, NextAuthOptions, SessionOptions, Theme } from ".."
|
||||
import { AppProvider } from "../providers"
|
||||
|
||||
/** Options that are the same both in internal and user provided options. */
|
||||
@@ -9,14 +9,22 @@ export type NextAuthSharedOptions =
|
||||
| "events"
|
||||
| "callbacks"
|
||||
| "cookies"
|
||||
| "secret"
|
||||
| "adapter"
|
||||
| "theme"
|
||||
| "debug"
|
||||
| "logger"
|
||||
|
||||
export interface AppOptions
|
||||
extends Pick<NextAuthOptions, NextAuthSharedOptions> {
|
||||
extends Required<Pick<NextAuthOptions, NextAuthSharedOptions>> {
|
||||
providers: AppProvider[]
|
||||
baseUrl: string
|
||||
basePath: string
|
||||
action:
|
||||
| "providers"
|
||||
| "session"
|
||||
| "csrf"
|
||||
| "signin"
|
||||
| "signout"
|
||||
| "callback"
|
||||
| "verify-request"
|
||||
| "error"
|
||||
pkce?: {
|
||||
code_verifier?: string
|
||||
/**
|
||||
@@ -27,20 +35,13 @@ export interface AppOptions
|
||||
code_challenge_method?: "S256"
|
||||
}
|
||||
provider?: AppProvider
|
||||
providers: AppProvider[]
|
||||
baseUrl?: string
|
||||
basePath?: string
|
||||
action?:
|
||||
| "providers"
|
||||
| "session"
|
||||
| "csrf"
|
||||
| "signin"
|
||||
| "signout"
|
||||
| "callback"
|
||||
| "verify-request"
|
||||
| "error"
|
||||
csrfToken?: string
|
||||
csrfTokenVerified?: boolean
|
||||
secret: string
|
||||
theme: Theme
|
||||
debug: boolean
|
||||
logger: LoggerInstance
|
||||
session: Required<SessionOptions>
|
||||
}
|
||||
|
||||
export interface NextAuthRequest extends NextApiRequest {
|
||||
|
||||
27
types/providers.d.ts
vendored
27
types/providers.d.ts
vendored
@@ -29,7 +29,7 @@ export interface OAuthConfig<P extends Record<string, unknown> = Profile>
|
||||
scope: string
|
||||
params: { grant_type: string }
|
||||
accessTokenUrl: string
|
||||
requestTokenUrl: string
|
||||
requestTokenUrl?: string
|
||||
authorizationUrl: string
|
||||
profileUrl: string
|
||||
profile(profile: P, tokens: TokenSet): Awaitable<User & { id: string }>
|
||||
@@ -67,6 +67,7 @@ export type OAuthProviderType =
|
||||
| "EVEOnline"
|
||||
| "Facebook"
|
||||
| "FACEIT"
|
||||
| "FortyTwo"
|
||||
| "Foursquare"
|
||||
| "FusionAuth"
|
||||
| "GitHub"
|
||||
@@ -77,6 +78,7 @@ export type OAuthProviderType =
|
||||
| "Kakao"
|
||||
| "LINE"
|
||||
| "LinkedIn"
|
||||
| "Mailchimp"
|
||||
| "MailRu"
|
||||
| "Medium"
|
||||
| "Netlify"
|
||||
@@ -90,6 +92,7 @@ export type OAuthProviderType =
|
||||
| "Twitch"
|
||||
| "Twitter"
|
||||
| "VK"
|
||||
| "WordPress"
|
||||
| "Yandex"
|
||||
| "Zoho"
|
||||
|
||||
@@ -130,19 +133,27 @@ export interface EmailConfigServerOptions {
|
||||
}
|
||||
}
|
||||
|
||||
export type SendVerificationRequest = (params: {
|
||||
identifier: string
|
||||
url: string
|
||||
baseUrl: string
|
||||
token: string
|
||||
provider: EmailConfig
|
||||
}) => Awaitable<void>
|
||||
|
||||
export interface EmailConfig extends CommonProviderOptions {
|
||||
type: "email"
|
||||
// TODO: Make use of https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html
|
||||
server: string | EmailConfigServerOptions
|
||||
/** @default "NextAuth <no-reply@example.com>" */
|
||||
from?: string
|
||||
/**
|
||||
* How long until the e-mail can be used to log the user in,
|
||||
* in seconds. Defaults to 1 day
|
||||
* @default 86400
|
||||
*/
|
||||
maxAge?: number
|
||||
sendVerificationRequest(params: {
|
||||
identifier: string
|
||||
url: string
|
||||
baseUrl: string
|
||||
token: string
|
||||
provider: EmailConfig
|
||||
}): Awaitable<void>
|
||||
sendVerificationRequest: SendVerificationRequest
|
||||
}
|
||||
|
||||
export type EmailProvider = (options: Partial<EmailConfig>) => EmailConfig
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import Providers, { AppProvider, OAuthConfig } from "next-auth/providers"
|
||||
import {
|
||||
Adapter,
|
||||
EmailAppProvider,
|
||||
Profile,
|
||||
Session,
|
||||
VerificationRequest,
|
||||
} from "next-auth/adapters"
|
||||
import Providers, {
|
||||
AppProvider,
|
||||
EmailConfig,
|
||||
OAuthConfig,
|
||||
} from "next-auth/providers"
|
||||
import { Adapter, AdapterInstance } from "next-auth/adapters"
|
||||
import NextAuth, * as NextAuthTypes from "next-auth"
|
||||
import { IncomingMessage, ServerResponse } from "http"
|
||||
import * as JWTType from "next-auth/jwt"
|
||||
@@ -54,74 +52,88 @@ const exampleUser: NextAuthTypes.User = {
|
||||
email: "",
|
||||
}
|
||||
|
||||
const exampleSession: Session = {
|
||||
const exampleSession: NextAuthTypes.Session = {
|
||||
userId: "",
|
||||
accessToken: "",
|
||||
sessionToken: "",
|
||||
expires: new Date(),
|
||||
}
|
||||
|
||||
const exampleVerificatoinRequest: VerificationRequest = {
|
||||
const exampleVerificationRequest = {
|
||||
id: "",
|
||||
identifier: "",
|
||||
token: "",
|
||||
expires: new Date(),
|
||||
}
|
||||
|
||||
const adapter: Adapter<
|
||||
NextAuthTypes.User,
|
||||
Profile,
|
||||
Session,
|
||||
VerificationRequest
|
||||
> = {
|
||||
async getAdapter(appOptions: AppOptions) {
|
||||
return {
|
||||
createUser: async (profile: Profile) => exampleUser,
|
||||
getUser: async (id: string) => exampleUser,
|
||||
getUserByEmail: async (email: string) => exampleUser,
|
||||
getUserByProviderAccountId: async (
|
||||
providerId: string,
|
||||
providerAccountId: string
|
||||
) => exampleUser,
|
||||
updateUser: async (user: NextAuthTypes.User) => exampleUser,
|
||||
linkAccount: async (
|
||||
userId: string,
|
||||
providerId: string,
|
||||
providerType: string,
|
||||
providerAccountId: string,
|
||||
refreshToken: string,
|
||||
accessToken: string,
|
||||
accessTokenExpires: number
|
||||
) => undefined,
|
||||
createSession: async (user: NextAuthTypes.User) => exampleSession,
|
||||
getSession: async (sessionToken: string) => exampleSession,
|
||||
updateSession: async (session: Session, force?: boolean) =>
|
||||
exampleSession,
|
||||
deleteSession: async (sessionToken: string) => undefined,
|
||||
createVerificationRequest: async (
|
||||
email: string,
|
||||
url: string,
|
||||
token: string,
|
||||
secret: string,
|
||||
provider: EmailAppProvider,
|
||||
options: AppOptions
|
||||
) => exampleVerificatoinRequest,
|
||||
getVerificationRequest: async (
|
||||
email: string,
|
||||
verificationToken: string,
|
||||
secret: string,
|
||||
provider: AppProvider
|
||||
) => exampleVerificatoinRequest,
|
||||
deleteVerificationRequest: async (
|
||||
email: string,
|
||||
verificationToken: string,
|
||||
secret: string,
|
||||
provider: AppProvider
|
||||
) => undefined,
|
||||
}
|
||||
},
|
||||
const MyAdapter: Adapter<Record<string, unknown>> = () => {
|
||||
return {
|
||||
async getAdapter(appOptions: AppOptions) {
|
||||
return {
|
||||
async createUser(profile) {
|
||||
return exampleUser
|
||||
},
|
||||
async getUser(id) {
|
||||
return exampleUser
|
||||
},
|
||||
async getUserByEmail(email) {
|
||||
return exampleUser
|
||||
},
|
||||
async getUserByProviderAccountId(providerId, providerAccountId) {
|
||||
return exampleUser
|
||||
},
|
||||
async updateUser(user) {
|
||||
return exampleUser
|
||||
},
|
||||
async linkAccount(
|
||||
userId,
|
||||
providerId,
|
||||
providerType,
|
||||
providerAccountId,
|
||||
refreshToken,
|
||||
accessToken,
|
||||
accessTokenExpires
|
||||
) {
|
||||
return undefined
|
||||
},
|
||||
async createSession(user) {
|
||||
return exampleSession
|
||||
},
|
||||
async getSession(sessionToken) {
|
||||
return exampleSession
|
||||
},
|
||||
async updateSession(session, force) {
|
||||
return exampleSession
|
||||
},
|
||||
async deleteSession(sessionToken) {
|
||||
return undefined
|
||||
},
|
||||
async createVerificationRequest(email, url, token, secret, provider) {
|
||||
return undefined
|
||||
},
|
||||
async getVerificationRequest(
|
||||
email,
|
||||
verificationToken,
|
||||
secret,
|
||||
provider
|
||||
) {
|
||||
return exampleVerificationRequest
|
||||
},
|
||||
async deleteVerificationRequest(
|
||||
email,
|
||||
verificationToken,
|
||||
secret,
|
||||
provider
|
||||
) {
|
||||
return undefined
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const allConfig = {
|
||||
const client = {} // Create a fake db client
|
||||
|
||||
const allConfig: NextAuthTypes.NextAuthOptions = {
|
||||
providers: [
|
||||
Providers.Twitter({
|
||||
clientId: "123",
|
||||
@@ -147,53 +159,40 @@ const allConfig = {
|
||||
},
|
||||
pages: pageOptions,
|
||||
callbacks: {
|
||||
async signIn(
|
||||
user: NextAuthTypes.User,
|
||||
account: Record<string, unknown>,
|
||||
profile: Record<string, unknown>
|
||||
) {
|
||||
async signIn(user, account, profile) {
|
||||
return true
|
||||
},
|
||||
async redirect(url: string, baseUrl: string) {
|
||||
async redirect(url, baseUrl) {
|
||||
return "path/to/foo"
|
||||
},
|
||||
async session(
|
||||
session: NextAuthTypes.Session,
|
||||
userOrToken: NextAuthTypes.User
|
||||
) {
|
||||
async session(session, userOrToken) {
|
||||
return { ...session }
|
||||
},
|
||||
async jwt(
|
||||
token: JWTType.JWT,
|
||||
user?: NextAuthTypes.User,
|
||||
account?: Record<string, unknown>,
|
||||
profile?: Record<string, unknown>,
|
||||
isNewUser?: boolean
|
||||
) {
|
||||
async jwt(token, user, account, profile, isNewUser) {
|
||||
return token
|
||||
},
|
||||
},
|
||||
events: {
|
||||
async signIn(message: string) {
|
||||
async signIn(message) {
|
||||
return undefined
|
||||
},
|
||||
async signOut(message: string) {
|
||||
async signOut(message) {
|
||||
return undefined
|
||||
},
|
||||
async createUser(message: string) {
|
||||
async createUser(message) {
|
||||
return undefined
|
||||
},
|
||||
async linkAccount(message: string) {
|
||||
async linkAccount(message) {
|
||||
return undefined
|
||||
},
|
||||
async session(message: string) {
|
||||
async session(message) {
|
||||
return undefined
|
||||
},
|
||||
async error(message: string) {
|
||||
async error(message) {
|
||||
return undefined
|
||||
},
|
||||
},
|
||||
adapter,
|
||||
adapter: MyAdapter(client),
|
||||
useSecureCookies: true,
|
||||
cookies: {
|
||||
sessionToken: {
|
||||
|
||||
@@ -68,7 +68,7 @@ See the [providers documentation](/configuration/providers) for a list of suppor
|
||||
|
||||
A random string used to hash tokens, sign cookies and generate crytographic keys.
|
||||
|
||||
If not specified is uses a hash of all configuration options, including Client ID / Secrets for entropy.
|
||||
If not specified, it uses a hash for all configuration options, including Client ID / Secrets for entropy.
|
||||
|
||||
The default behaviour is volatile, and it is strongly recommended you explicitly specify a value to avoid invalidating end user sessions when configuration changes are deployed.
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ By default, the built-in pages will follow the system theme, utilizing the [`pre
|
||||
In order to get the available authentication providers and the URLs to use for them, you can make a request to the API endpoint `/api/auth/providers`:
|
||||
|
||||
```jsx title="pages/auth/signin.js"
|
||||
import { providers, signIn } from 'next-auth/client'
|
||||
import { getProviders, signIn } from 'next-auth/client'
|
||||
|
||||
export default function SignIn({ providers }) {
|
||||
return (
|
||||
@@ -76,7 +76,7 @@ export default function SignIn({ providers }) {
|
||||
|
||||
// This is the recommended way for Next.js 9.3 or newer
|
||||
export async function getServerSideProps(context){
|
||||
const providers = await providers()
|
||||
const providers = await getProviders()
|
||||
return {
|
||||
props: { providers }
|
||||
}
|
||||
@@ -86,7 +86,7 @@ export async function getServerSideProps(context){
|
||||
// If older than Next.js 9.3
|
||||
SignIn.getInitialProps = async () => {
|
||||
return {
|
||||
providers: await providers()
|
||||
providers: await getProviders()
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -3,59 +3,123 @@ id: providers
|
||||
title: Providers
|
||||
---
|
||||
|
||||
Authentication Providers in NextAuth.js are services that can be used to sign in (OAuth, Email, etc).
|
||||
Authentication Providers in **NextAuth.js** are services that can be used to sign in a user.
|
||||
|
||||
## Sign in with OAuth
|
||||
There's four ways a user can be signed in:
|
||||
|
||||
NextAuth.js is designed to work with any OAuth service, it supports OAuth 1.0, 1.0A and 2.0 and has built-in support for many popular OAuth sign-in services.
|
||||
- [Using a built-in OAuth Provider](#oauth-providers) (e.g Github, Twitter, Google, etc...)
|
||||
- [Using a custom OAuth Provider](#-using-a-custom-provider)
|
||||
- [Using Email](#email-provider)
|
||||
- [Using Credentials](#credentials-provider)
|
||||
|
||||
### Built-in OAuth providers
|
||||
:::note
|
||||
NextAuth.js is designed to work with any OAuth service, it supports **OAuth 1.0**, **1.0A** and **2.0** and has built-in support for most popular sign-in services.
|
||||
:::
|
||||
|
||||
<ul>
|
||||
## OAuth Providers
|
||||
|
||||
### Available providers
|
||||
|
||||
<div className="provider-name-list">
|
||||
{Object.entries(require("../../providers.json"))
|
||||
.filter(([key]) => !["email", "credentials"].includes(key))
|
||||
.sort(([, a], [, b]) => a.localeCompare(b))
|
||||
.map(([key, name]) =>
|
||||
<li key={key}><a href={`/providers/${key}`}>{name}</a></li>
|
||||
.map(([key, name]) => (
|
||||
<span key={key}>
|
||||
<a href={`/providers/${key}`}>{name}</a>
|
||||
<span className="provider-name-list__comma">,</span>
|
||||
</span>
|
||||
)
|
||||
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
### Using a built-in OAuth provider
|
||||
### How to
|
||||
|
||||
1. Register your application at the developer portal of your provider. There are links above to the developer docs for most supported providers with details on how to register your application.
|
||||
|
||||
2. The redirect URI should follow this format:
|
||||
```
|
||||
[origin]/api/auth/callback/[provider]
|
||||
```
|
||||
For example, Twitter on `localhost` this would be:
|
||||
```
|
||||
http://localhost:3000/api/auth/callback/twitter
|
||||
```
|
||||
|
||||
```
|
||||
[origin]/api/auth/callback/[provider]
|
||||
```
|
||||
|
||||
For example, Twitter on `localhost` this would be:
|
||||
|
||||
```
|
||||
http://localhost:3000/api/auth/callback/twitter
|
||||
```
|
||||
|
||||
3. Create a `.env` file at the root of your project and add the client ID and client secret. For Twitter this would be:
|
||||
|
||||
```
|
||||
TWITTER_ID=YOUR_TWITTER_CLIENT_ID
|
||||
TWITTER_SECRET=YOUR_TWITTER_CLIENT_SECRET
|
||||
```
|
||||
```
|
||||
TWITTER_ID=YOUR_TWITTER_CLIENT_ID
|
||||
TWITTER_SECRET=YOUR_TWITTER_CLIENT_SECRET
|
||||
```
|
||||
|
||||
4. Now you can add the provider settings to the NextAuth options object. You can add as many OAuth providers as you like, as you can see `providers` is an array.
|
||||
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
import Providers from `next-auth/providers`
|
||||
...
|
||||
providers: [
|
||||
Providers.Twitter({
|
||||
clientId: process.env.TWITTER_ID,
|
||||
clientSecret: process.env.TWITTER_SECRET
|
||||
})
|
||||
],
|
||||
...
|
||||
```
|
||||
5. Once a provider has been setup, you can sign in at the following URL: `[origin]/api/auth/signin`. This is an unbranded auto-generated page with all the configured providers.
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
import Providers from `next-auth/providers`
|
||||
...
|
||||
providers: [
|
||||
Providers.Twitter({
|
||||
clientId: process.env.TWITTER_ID,
|
||||
clientSecret: process.env.TWITTER_SECRET
|
||||
})
|
||||
],
|
||||
...
|
||||
```
|
||||
|
||||
5. Once a provider has been setup, you can sign in at the following URL: `[origin]/api/auth/signin`. This is an unbranded auto-generated page with all the configured providers.
|
||||
|
||||
<Image src="/img/signin.png" alt="Signin Screenshot" />
|
||||
|
||||
### Options
|
||||
|
||||
| Name | Description | Type | Required |
|
||||
| :-----------------: | :--------------------------------------------------------------: | :---------------------------: | :------: |
|
||||
| id | Unique ID for the provider | `string` | Yes |
|
||||
| name | Descriptive name for the provider | `string` | Yes |
|
||||
| type | Type of provider, in this case `oauth` | `"oauth"` | Yes |
|
||||
| version | OAuth version (e.g. '1.0', '1.0a', '2.0') | `string` | Yes |
|
||||
| scope | OAuth access scopes (expects array or string) | `string` or `string[]` | Yes |
|
||||
| params | Extra URL params sent when calling `accessTokenUrl` | `Object` | Yes |
|
||||
| accessTokenUrl | Endpoint to retrieve an access token | `string` | Yes |
|
||||
| authorizationUrl | Endpoint to request authorization from the user | `string` | Yes |
|
||||
| requestTokenUrl | Endpoint to retrieve a request token | `string` | Yes |
|
||||
| profileUrl | Endpoint to retrieve the user's profile | `string` | Yes |
|
||||
| clientId | Client ID of the OAuth provider | `string` | Yes |
|
||||
| clientSecret | Client Secret of the OAuth provider | `string` | Yes |
|
||||
| profile | A callback returning an object with the user's info | `(profile, tokens) => Object` | Yes |
|
||||
| protection | Additional security for OAuth login flows (defaults to `state`) | `"pkce"`,`"state"`,`"none"` | No |
|
||||
| state | Same as `protection: "state"`. Being deprecated, use protection. | `boolean` | No |
|
||||
| headers | Any headers that should be sent to the OAuth provider | `Object` | No |
|
||||
| authorizationParams | Additional params to be sent to the authorization endpoint | `Object` | No |
|
||||
| idToken | Set to `true` for services that use ID Tokens (e.g. OpenID) | `boolean` | No |
|
||||
| region | Only when using BattleNet | `string` | No |
|
||||
| domain | Only when using certain Providers | `string` | No |
|
||||
| tenantId | Only when using Azure, Active Directory, B2C, FusionAuth | `string` | No |
|
||||
|
||||
:::tip
|
||||
Even if you are using a built-in provider, you can override any of these options to tweak the default configuration.
|
||||
|
||||
```js title=[...nextauth].js
|
||||
import Providers from "next-auth/providers"
|
||||
|
||||
Providers.Auth0({
|
||||
clientId: process.env.CLIENT_ID,
|
||||
clientSecret: process.env.CLIENT_SECRET,
|
||||
domain: process.env.DOMAIN,
|
||||
scope: "openid your_custom_scope", // We do provide a default, but this will override it if defined
|
||||
profile(profile) {
|
||||
return {} // Return the profile in a shape that is different from the built-in one.
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### Using a custom provider
|
||||
|
||||
You can use an OAuth provider that isn't built-in by using a custom object.
|
||||
@@ -76,7 +140,7 @@ As an example of what this looks like, this is the provider object returned for
|
||||
profileUrl: "https://www.googleapis.com/oauth2/v1/userinfo?alt=json",
|
||||
async profile(profile, tokens) {
|
||||
// You can use the tokens, in case you want to fetch more profile information
|
||||
// For example several OAuth provider does not return e-mail by default.
|
||||
// For example several OAuth providers do not return email by default.
|
||||
// Depending on your provider, will have tokens like `access_token`, `id_token` and or `refresh_token`
|
||||
return {
|
||||
id: profile.id,
|
||||
@@ -89,7 +153,8 @@ As an example of what this looks like, this is the provider object returned for
|
||||
clientSecret: ""
|
||||
}
|
||||
```
|
||||
You can replace all the options in this JSON object with the ones from your custom provider - be sure to give it a unique ID and specify the correct OAuth version - and add it to the providers option:
|
||||
|
||||
Replace all the options in this JSON object with the ones from your custom provider - be sure to give it a unique ID and specify the correct OAuth version - and add it to the providers option when initializing the library:
|
||||
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
import Providers from `next-auth/providers`
|
||||
@@ -111,33 +176,24 @@ providers: [
|
||||
...
|
||||
```
|
||||
|
||||
### Adding a new provider
|
||||
|
||||
If you think your custom provider might be useful to others, we encourage you to open a PR and add it to the built-in list so others can discover it much more easily!
|
||||
|
||||
You only need to add two changes:
|
||||
|
||||
### OAuth provider options
|
||||
1. Add your config: [`src/providers/{provider}.js`](https://github.com/nextauthjs/next-auth/tree/main/src/providers)<br />
|
||||
• make sure you use a named default export, like this: `export default function YourProvider`
|
||||
2. Add provider documentation: [`www/docs/providers/{provider}.md`](https://github.com/nextauthjs/next-auth/tree/main/www/docs/providers)
|
||||
3. Add it to our [provider types](https://github.com/nextauthjs/next-auth/blob/main/types/providers.d.ts) (for TS projects)<br />
|
||||
• you just need to add your new provider name to [this list](https://github.com/nextauthjs/next-auth/blob/main/types/providers.d.ts#L56-L97)<br />
|
||||
• in case you new provider accepts some custom options, you can [add them here](https://github.com/nextauthjs/next-auth/blob/main/types/providers.d.ts#L48-L53)
|
||||
|
||||
| Name | Description | Type | Required |
|
||||
| :-----------------: | :--------------------------------------------------------------: | :-----------------------------: | :------: |
|
||||
| id | Unique ID for the provider | `string` | Yes |
|
||||
| name | Descriptive name for the provider | `string` | Yes |
|
||||
| type | Type of provider, in this case it should be `oauth` | `oauth`, `email`, `credentials` | Yes |
|
||||
| version | OAuth version (e.g. '1.0', '1.0a', '2.0') | `string` | Yes |
|
||||
| accessTokenUrl | Endpoint to retrieve an access token | `string` | Yes |
|
||||
| authorizationUrl | Endpoint to request authorization from the user | `string` | Yes |
|
||||
| clientId | Client ID of the OAuth provider | `string` | Yes |
|
||||
| clientSecret | Client Secret of the OAuth provider | `string` | No |
|
||||
| scope | OAuth access scopes (expects array or string) | `string` or `string[]` | No |
|
||||
| params | Additional authorization URL parameters | `object` | No |
|
||||
| requestTokenUrl | Endpoint to retrieve a request token | `string` | No |
|
||||
| authorizationParams | Additional params to be sent to the authorization endpoint | `object` | No |
|
||||
| profileUrl | Endpoint to retrieve the user's profile | `string` | No |
|
||||
| profile | An callback returning an object with the user's info | `object` | No |
|
||||
| idToken | Set to `true` for services that use ID Tokens (e.g. OpenID) | `boolean` | No |
|
||||
| headers | Any headers that should be sent to the OAuth provider | `object` | No |
|
||||
| protection | Additional security for OAuth login flows (defaults to `state`) |`[pkce]`,`[state]`,`[pkce,state]`| No |
|
||||
| state | Same as `protection: "state"`. Being deprecated, use protection. | `boolean` | No |
|
||||
That's it! 🎉 Others will be able to discover this provider much more easily now!
|
||||
|
||||
## Sign in with Email
|
||||
## Email Provider
|
||||
|
||||
### How to
|
||||
|
||||
The Email provider uses email to send "magic links" that can be used sign in, you will likely have seen them before if you have used software like Slack.
|
||||
|
||||
@@ -164,8 +220,21 @@ See the [Email provider documentation](/providers/email) for more information on
|
||||
The email provider requires a database, it cannot be used without one.
|
||||
:::
|
||||
|
||||
### Options
|
||||
|
||||
## Sign in with Credentials
|
||||
| Name | Description | Type | Required |
|
||||
| :---------------------: | :---------------------------------------------------------------------------------: | :------------------------------: | :------: |
|
||||
| id | Unique ID for the provider | `string` | Yes |
|
||||
| name | Descriptive name for the provider | `string` | Yes |
|
||||
| type | Type of provider, in this case `email` | `"email"` | Yes |
|
||||
| server | Path or object pointing to the email server | `string` or `Object` | Yes |
|
||||
| sendVerificationRequest | Callback to execute when a verification request is sent | `(params) => Promise<undefined>` | Yes |
|
||||
| from | The email address from which emails are sent, default: "<no-reply@example.com>" | `string` | No |
|
||||
| maxAge | How long until the e-mail can be used to log the user in seconds. Defaults to 1 day | `number` | No |
|
||||
|
||||
## Credentials Provider
|
||||
|
||||
### How to
|
||||
|
||||
The Credentials provider allows you to handle signing in with arbitrary credentials, such as a username and password, two factor authentication or hardware device (e.g. YubiKey U2F / FIDO).
|
||||
|
||||
@@ -211,26 +280,12 @@ See the [Credentials provider documentation](/providers/credentials) for more in
|
||||
The Credentials provider can only be used if JSON Web Tokens are enabled for sessions. Users authenticated with the Credentials provider are not persisted in the database.
|
||||
:::
|
||||
|
||||
<!-- React Image Component -->
|
||||
export const Image = ({ children, src, alt = '' }) => (
|
||||
<div
|
||||
style={{
|
||||
padding: '0.2rem',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<img alt={alt} src={src} />
|
||||
</div>
|
||||
)
|
||||
### Options
|
||||
|
||||
|
||||
## Adding a new built-in provider
|
||||
|
||||
If you think your custom provider might be useful to others, we encourage you to open a PR and add it to the built-in list so others can discover it much more easily! You only need to add two changes:
|
||||
1. Add your config: [`src/providers/{provider}.js`](https://github.com/nextauthjs/next-auth/tree/main/src/providers) (Make sure you use a named default export, like `export default function YourProvider`!)
|
||||
2. Add provider documentation: [`www/docs/providers/{provider}.md`](https://github.com/nextauthjs/next-auth/tree/main/www/docs/providers)
|
||||
|
||||
That's it! 🎉 Others will be able to discover this provider much more easily now!
|
||||
|
||||
You can look at the existing built-in providers for inspiration.
|
||||
| Name | Description | Type | Required |
|
||||
| :---------: | :-----------------------------------------------: | :------------------------------: | :------: |
|
||||
| id | Unique ID for the provider | `string` | Yes |
|
||||
| name | Descriptive name for the provider | `string` | Yes |
|
||||
| type | Type of provider, in this case `credentials` | `"credentials"` | Yes |
|
||||
| credentials | The credentials to sign-in with | `Object` | Yes |
|
||||
| authorize | Callback to execute once user is to be authorized | `(credentials) => Promise<User>` | Yes |
|
||||
|
||||
@@ -95,7 +95,7 @@ If you are unable to use an HS512 key (for example to interoperate with other se
|
||||
|
||||
````
|
||||
jwt: {
|
||||
signingKey: {"kty":"oct","kid":"--","alg":"HS256","k":"--"}
|
||||
signingKey: {"kty":"oct","kid":"--","alg":"HS256","k":"--"},
|
||||
verificationOptions: {
|
||||
algorithms: ["HS256"]
|
||||
}
|
||||
|
||||
26
www/docs/providers/42.md
Normal file
26
www/docs/providers/42.md
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
id: 42-school
|
||||
title: 42 School
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
https://api.intra.42.fr/apidoc/guides/web_application_flow
|
||||
|
||||
## Configuration
|
||||
|
||||
https://profile.intra.42.fr/oauth/applications/new
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
...
|
||||
providers: [
|
||||
Providers.FortyTwo({
|
||||
clientId: process.env.FORTY_TWO_CLIENT_ID,
|
||||
clientSecret: process.env.FORTY_TWO_CLIENT_SECRET
|
||||
})
|
||||
]
|
||||
...
|
||||
```
|
||||
26
www/docs/providers/mailchimp.md
Normal file
26
www/docs/providers/mailchimp.md
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
id: mailchimp
|
||||
title: Mailchimp
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
https://mailchimp.com/developer/marketing/guides/access-user-data-oauth-2/
|
||||
|
||||
## Configuration
|
||||
|
||||
https://admin.mailchimp.com/account/oauth2/client/
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
...
|
||||
providers: [
|
||||
Providers.Mailchimp({
|
||||
clientId: process.env.MAILCHIMP_CLIENT_ID,
|
||||
clientSecret: process.env.MAILCHIMP_CLIENT_SECRET
|
||||
})
|
||||
]
|
||||
...
|
||||
```
|
||||
30
www/docs/providers/wordpress.md
Normal file
30
www/docs/providers/wordpress.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
id: wordpress
|
||||
title: WordPress.com
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
https://developer.wordpress.com/docs/oauth2/
|
||||
|
||||
## Configuration
|
||||
|
||||
https://developer.wordpress.com/apps/
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
...
|
||||
providers: [
|
||||
Providers.WordPress({
|
||||
clientId: process.env.WORDPRESS_CLIENT_ID,
|
||||
clientSecret: process.env.WORDPRESS_CLIENT_SECRET
|
||||
})
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
:::tip
|
||||
Register your application to obtain Client ID and Client Secret at https://developer.wordpress.com/apps/ Select Type as Web and set Redirect URL to `http://example.com/api/auth/callback/wordpress` where example.com is your site domain.
|
||||
:::
|
||||
@@ -188,7 +188,7 @@ npx prisma generate
|
||||
To configure you database to use the new schema (i.e. create tables and columns) use the `prisma migrate` command:
|
||||
|
||||
```
|
||||
npx prisma migrate dev --preview-feature
|
||||
npx prisma migrate dev
|
||||
```
|
||||
|
||||
To generate a schema in this way with the above example code, you will need to specify your datbase connection string in the environment variable `DATABASE_URL`. You can do this by setting it in a `.env` file at the root of your project.
|
||||
|
||||
@@ -40,141 +40,95 @@ These methods are required to support email / passwordless sign in:
|
||||
|
||||
These methods will be required in a future release, but are not yet invoked:
|
||||
|
||||
* getUserByCredentials
|
||||
* deleteUser
|
||||
* unlinkAccount
|
||||
|
||||
### Example code
|
||||
|
||||
```js
|
||||
const Adapter = (config, options = {}) => {
|
||||
|
||||
async function getAdapter (appOptions) {
|
||||
|
||||
async function createUser (profile) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUser (id) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUserByEmail (email) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUserByProviderAccountId (
|
||||
providerId,
|
||||
providerAccountId
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUserByCredentials (credentials) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function updateUser (user) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function deleteUser (userId) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function linkAccount (
|
||||
userId,
|
||||
providerId,
|
||||
providerType,
|
||||
providerAccountId,
|
||||
refreshToken,
|
||||
accessToken,
|
||||
accessTokenExpires
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function unlinkAccount (
|
||||
userId,
|
||||
providerId,
|
||||
providerAccountId
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function createSession (user) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function getSession (sessionToken) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function updateSession (
|
||||
session,
|
||||
force
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function deleteSession (sessionToken) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function createVerificationRequest (
|
||||
identifier,
|
||||
url,
|
||||
token,
|
||||
secret,
|
||||
provider
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function getVerificationRequest (
|
||||
identifier,
|
||||
token,
|
||||
secret,
|
||||
provider
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function deleteVerificationRequest (
|
||||
identifier,
|
||||
token,
|
||||
secret,
|
||||
provider
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
createUser,
|
||||
getUser,
|
||||
getUserByEmail,
|
||||
getUserByProviderAccountId,
|
||||
getUserByCredentials,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
linkAccount,
|
||||
unlinkAccount,
|
||||
createSession,
|
||||
getSession,
|
||||
updateSession,
|
||||
deleteSession,
|
||||
createVerificationRequest,
|
||||
getVerificationRequest,
|
||||
deleteVerificationRequest
|
||||
}
|
||||
}
|
||||
|
||||
export default function YourAdapter (config, options = {}) {
|
||||
return {
|
||||
getAdapter
|
||||
async getAdapter (appOptions) {
|
||||
async createUser (profile) {
|
||||
return null
|
||||
},
|
||||
async getUser (id) {
|
||||
return null
|
||||
},
|
||||
async getUserByEmail (email) {
|
||||
return null
|
||||
},
|
||||
async getUserByProviderAccountId (
|
||||
providerId,
|
||||
providerAccountId
|
||||
) {
|
||||
return null
|
||||
},
|
||||
async updateUser (user) {
|
||||
return null
|
||||
},
|
||||
async deleteUser (userId) {
|
||||
return null
|
||||
},
|
||||
async linkAccount (
|
||||
userId,
|
||||
providerId,
|
||||
providerType,
|
||||
providerAccountId,
|
||||
refreshToken,
|
||||
accessToken,
|
||||
accessTokenExpires
|
||||
) {
|
||||
return null
|
||||
},
|
||||
async unlinkAccount (
|
||||
userId,
|
||||
providerId,
|
||||
providerAccountId
|
||||
) {
|
||||
return null
|
||||
},
|
||||
async createSession (user) {
|
||||
return null
|
||||
},
|
||||
async getSession (sessionToken) {
|
||||
return null
|
||||
},
|
||||
async updateSession (
|
||||
session,
|
||||
force
|
||||
) {
|
||||
return null
|
||||
},
|
||||
async deleteSession (sessionToken) {
|
||||
return null
|
||||
},
|
||||
async createVerificationRequest (
|
||||
identifier,
|
||||
url,
|
||||
token,
|
||||
secret,
|
||||
provider
|
||||
) {
|
||||
return null
|
||||
},
|
||||
async getVerificationRequest (
|
||||
identifier,
|
||||
token,
|
||||
secret,
|
||||
provider
|
||||
) {
|
||||
return null
|
||||
},
|
||||
async deleteVerificationRequest (
|
||||
identifier,
|
||||
token,
|
||||
secret,
|
||||
provider
|
||||
) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
Adapter
|
||||
}
|
||||
```
|
||||
|
||||
6
www/package-lock.json
generated
6
www/package-lock.json
generated
@@ -17485,9 +17485,9 @@
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
|
||||
},
|
||||
"ssri": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz",
|
||||
"integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==",
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz",
|
||||
"integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==",
|
||||
"requires": {
|
||||
"figgy-pudding": "^3.5.1"
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ html[data-theme="dark"]:root {
|
||||
@import "buttons.css";
|
||||
@import "table-of-contents.css";
|
||||
@import "sidebar.css";
|
||||
@import "providers.css";
|
||||
|
||||
@media screen and (max-width: 360px) {
|
||||
html {
|
||||
@@ -89,6 +90,12 @@ a:hover,
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 996px) {
|
||||
.main-wrapper > div {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.docusaurus-highlight-code-line {
|
||||
background-color: rgb(72, 77, 91);
|
||||
display: block;
|
||||
|
||||
9
www/src/css/providers.css
Normal file
9
www/src/css/providers.css
Normal file
@@ -0,0 +1,9 @@
|
||||
.provider-name-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.provider-name-list__comma {
|
||||
display: inline-flex;
|
||||
margin-right: 5px;
|
||||
}
|
||||
Reference in New Issue
Block a user