mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
121 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac5b4db0f2 | ||
|
|
8bbffdd08c | ||
|
|
a22a0a36fd | ||
|
|
797272afe1 | ||
|
|
13e56bcf2f | ||
|
|
b0f7f87c04 | ||
|
|
9c0851c0f9 | ||
|
|
f5b3c29ab1 | ||
|
|
b4f2a0106a | ||
|
|
9c095b0532 | ||
|
|
0475964a0f | ||
|
|
ad6c13cdc9 | ||
|
|
591aa7cc7e | ||
|
|
9abb392b4e | ||
|
|
b89ae87fb1 | ||
|
|
3687d17724 | ||
|
|
b04ff82fb9 | ||
|
|
c11915ba9c | ||
|
|
24ee459f97 | ||
|
|
ac4851d238 | ||
|
|
84094b0ee7 | ||
|
|
f09ab4a04f | ||
|
|
067364381b | ||
|
|
6ee36b6842 | ||
|
|
5a89ab69d3 | ||
|
|
665445818e | ||
|
|
67cf2a11bb | ||
|
|
832d51f10e | ||
|
|
29862ac887 | ||
|
|
5aa2b61b88 | ||
|
|
929c644653 | ||
|
|
2657e72e81 | ||
|
|
8ff7dbb18f | ||
|
|
748d576a5a | ||
|
|
9f16e3f0fb | ||
|
|
1042e9a93d | ||
|
|
aa57f2dd7e | ||
|
|
1817286ce3 | ||
|
|
b942dd34f3 | ||
|
|
4d9622e1cc | ||
|
|
a7eadf80e5 | ||
|
|
75c7dbc3e7 | ||
|
|
d36b89cb12 | ||
|
|
349cd03fbd | ||
|
|
5cd130669b | ||
|
|
638233f4a0 | ||
|
|
37e175195f | ||
|
|
e8a9e8aeb6 | ||
|
|
1fb308a6f4 | ||
|
|
613c303315 | ||
|
|
d24fe1cebb | ||
|
|
885b02ca95 | ||
|
|
f218697fd6 | ||
|
|
dbead0ad85 | ||
|
|
704ded5310 | ||
|
|
25fbcb4648 | ||
|
|
53a439b44b | ||
|
|
16a2e37fd6 | ||
|
|
0392a8df9a | ||
|
|
a459b95c5b | ||
|
|
13df7eb81d | ||
|
|
62f261209c | ||
|
|
da43d0d896 | ||
|
|
4b1271ba75 | ||
|
|
d30da0170f | ||
|
|
887b2985fc | ||
|
|
d2bbac1164 | ||
|
|
35583a513d | ||
|
|
665d91019f | ||
|
|
f2b816b7b9 | ||
|
|
2e770fb0bf | ||
|
|
e83e7231fb | ||
|
|
4593ec8b01 | ||
|
|
12517f629b | ||
|
|
77012bc00c | ||
|
|
60fdf26a56 | ||
|
|
0fae0c7a8e | ||
|
|
eba79f4445 | ||
|
|
e3bb9881ea | ||
|
|
827049cb35 | ||
|
|
ad8100d402 | ||
|
|
7b5defff16 | ||
|
|
bc9805d1ba | ||
|
|
c823016b36 | ||
|
|
ca0f4c6fba | ||
|
|
c0d2f2d852 | ||
|
|
71f63117a9 | ||
|
|
d04ce29314 | ||
|
|
d2882f1958 | ||
|
|
66db563ca5 | ||
|
|
9619077363 | ||
|
|
013ccb4cb0 | ||
|
|
6eb41259d1 | ||
|
|
141f8d07e2 | ||
|
|
ffd0601ab0 | ||
|
|
7864d4705d | ||
|
|
98dc82e5d6 | ||
|
|
86baefdd9d | ||
|
|
332e237c3e | ||
|
|
2fce08c0b5 | ||
|
|
adf3fb669f | ||
|
|
5323be3594 | ||
|
|
6df0d04a1e | ||
|
|
aa9c1e7c96 | ||
|
|
66473054f5 | ||
|
|
e8ddbc5c11 | ||
|
|
dfe4620056 | ||
|
|
848224e2c5 | ||
|
|
aee376cc57 | ||
|
|
0d2a81cd39 | ||
|
|
61e99c9489 | ||
|
|
0eb4159737 | ||
|
|
9f0008375f | ||
|
|
0cf1823e70 | ||
|
|
7f39669053 | ||
|
|
7b82d6e985 | ||
|
|
53b0a7aa74 | ||
|
|
fbb09303af | ||
|
|
ff05ac1e41 | ||
|
|
a6f6c1590d | ||
|
|
52c2466b9e |
@@ -1,4 +0,0 @@
|
||||
# Exclude directories we don't need from Docker context to improve build time
|
||||
node_modules
|
||||
www
|
||||
src
|
||||
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1,3 +1,4 @@
|
||||
# https://docs.github.com/en/github/administering-a-repository/displaying-a-sponsor-button-in-your-repository
|
||||
|
||||
open_collective: nextauth
|
||||
github: [balazsorban44]
|
||||
|
||||
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,34 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Report a defect with NextAuth.js
|
||||
labels: bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of the bug in NextAuth.js.
|
||||
|
||||
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.
|
||||
|
||||
**Steps to reproduce**
|
||||
Steps to reproduce the behavior.
|
||||
|
||||
Include a link to public repository which can be used to reproduce the behaviour.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots or error logs**
|
||||
If applicable add screenshots or error logs to help explain the problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
**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).*
|
||||
|
||||
* [ ] 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
|
||||
91
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
91
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
name: Bug Report
|
||||
description: File a bug report
|
||||
labels: bug
|
||||
|
||||
# note: markdown sections will NOT appear as part of the issue as per documentation, rather they provide context to the user
|
||||
# https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#markdown
|
||||
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report! Please provide the following information:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description 🐜
|
||||
description: Please provide a clear and concise description of the bug in NextAuth.js
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: ownproject
|
||||
attributes:
|
||||
label: Is this a bug in your own project?
|
||||
description: 🚧 – _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) or use the [Discussions tab](https://github.com/nextauthjs/next-auth/discussions) - this helps us reduce the maintenance overhead._
|
||||
multiple: false
|
||||
options:
|
||||
- "Yes"
|
||||
- "No"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: How to reproduce ☕️
|
||||
description: Please provide a link or code snippets to a minimal reproduction of the bug
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
We encourage you to use one of the templates set up on **CodeSandbox** to reproduce your issue:
|
||||
- [`next-auth-example`](https://codesandbox.io/s/next-auth-example-1kktb)
|
||||
- [`next-auth-typescript-example`](https://codesandbox.io/s/next-auth-typescript-example-se32w)
|
||||
|
||||
🚧 – _If you don't provide any way to reproduce the bug, the issue is at risk of being closed._
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Screenshots / Logs 📽
|
||||
description: We can address the bug you found much faster if you provide contextual screenshots or screen recordings showcasing the issue.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
See [Kap](https://getkap.co/) for a good, easy-to-use, cross-platform screen recording tool.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment 🖥
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please run this command in your project's root folder:
|
||||
|
||||
```sh
|
||||
npx envinfo --system --binaries --browsers --npmPackages "next,next-auth,react"
|
||||
```
|
||||
|
||||
- type: dropdown
|
||||
id: pr
|
||||
attributes:
|
||||
label: Contributing 🙌🏽
|
||||
multiple: false
|
||||
options:
|
||||
- "Yes, I am willing to help solve this bug in a PR"
|
||||
- "No, I am afraid I cannot help regarding this"
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
It takes a lot of work 🏋🏻♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
|
||||
|
||||
26
.github/ISSUE_TEMPLATE/feature_request.md
vendored
26
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,26 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for NextAuth.js
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Summary of proposed feature**
|
||||
A clear and concise description of the feature being proposed.
|
||||
|
||||
**Purpose of proposed feature**
|
||||
A clear and concise description of why this feature is necessary and what problems it solves.
|
||||
|
||||
**Detail about proposed feature**
|
||||
A detailed description of how the proposal might work (if you have one).
|
||||
|
||||
**Potential problems**
|
||||
Describe any potential problems or potential limitations or caveats that might apply to the proposed solution.
|
||||
|
||||
**Describe any alternatives you've considered**
|
||||
A clear and concise description of any alternative options you've considered.
|
||||
|
||||
**Additional context**
|
||||
Any other context, screenshots, etc.
|
||||
|
||||
*Please indicate if you are willing and able to help implement the proposed feature.*
|
||||
68
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
68
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
name: Feature Request
|
||||
description: Suggest an idea for NextAuth.js
|
||||
labels: enhancement
|
||||
|
||||
# note: markdown sections will NOT appear as part of the issue as per documentation, rather they provide context to the user
|
||||
# https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#markdown
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you very much for reaching out to us regarding the awesome feature that you believe should be included in the NextAuth.js library. Please provide the following information:
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description 📓
|
||||
description: Please provide a more in-depth description of the feature proposed.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Make sure you provide plenty of [links]() to external documentation and inline code examples like so:
|
||||
|
||||
```js
|
||||
function myAwesomeNextAuthFeature() {
|
||||
return 💚
|
||||
}
|
||||
```
|
||||
|
||||
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**
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: How to reproduce ☕️
|
||||
description: If you have a CodeSandbox playground or some code snippets to help us visualize your idea better, please provide it here.
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
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)
|
||||
|
||||
- type: dropdown
|
||||
id: pr
|
||||
attributes:
|
||||
label: Contributing 🙌🏽
|
||||
multiple: false
|
||||
options:
|
||||
- "Yes, I am willing to help implement this feature in a PR"
|
||||
- "No, I am afraid I cannot help regarding this"
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
It takes a lot of work 🏋🏻♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
|
||||
|
||||
25
.github/ISSUE_TEMPLATE/question.md
vendored
25
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -1,25 +0,0 @@
|
||||
---
|
||||
name: Question
|
||||
about: Ask a question about NextAuth.js or for help using it
|
||||
labels: question
|
||||
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. -->
|
||||
|
||||
**What are you trying to do**
|
||||
<!-- A description of what you are trying to do, for context. -->
|
||||
|
||||
**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. -->
|
||||
|
||||
**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).*
|
||||
|
||||
* [ ] 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
|
||||
62
.github/ISSUE_TEMPLATE/question.yaml
vendored
Normal file
62
.github/ISSUE_TEMPLATE/question.yaml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
name: Question
|
||||
description: Ask a question about NextAuth.js or for help using it
|
||||
labels: question
|
||||
|
||||
# note: markdown sections will NOT appear as part of the issue as per documentation, rather they provide context to the user
|
||||
# https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#markdown
|
||||
|
||||
body:
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
We are glad that you have a question about this library. Please provide the following information:
|
||||
|
||||
- type: textarea
|
||||
id: question
|
||||
attributes:
|
||||
label: Question 💬
|
||||
description: Please provide an in-depth description of the question you have.
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
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)!
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: How to reproduce ☕️
|
||||
description: Please provide a link to a minimal reproduction or code snippets that represents your question
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
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)
|
||||
|
||||
- type: dropdown
|
||||
id: pr
|
||||
attributes:
|
||||
label: Contributing 🙌🏽
|
||||
multiple: false
|
||||
options:
|
||||
- "Yes, I am willing to help answer this question in a PR"
|
||||
- "No, I am afraid I cannot help regarding this"
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
It takes a lot of work 🏋🏻♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
|
||||
|
||||
58
.github/ISSUE_TEMPLATE/typescript.yaml
vendored
Normal file
58
.github/ISSUE_TEMPLATE/typescript.yaml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
name: TypeScript
|
||||
description: Ask a question about NextAuth.js TypeScript integration
|
||||
labels: [question, TypeScript]
|
||||
assignees: [lluia, balazsorban44]
|
||||
|
||||
# note: markdown sections will NOT appear as part of the issue as per documentation, rather they provide context to the user
|
||||
# https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#markdown
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
id: question
|
||||
attributes:
|
||||
label: Question 💬
|
||||
description: 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`.
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
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)!
|
||||
|
||||
- type: textarea
|
||||
id: codesandbox
|
||||
attributes:
|
||||
label: How to reproduce ☕️
|
||||
description: Please provide a link to a minimal reproduction or code snippets that represents your question
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
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)
|
||||
|
||||
|
||||
- type: dropdown
|
||||
id: pr
|
||||
attributes:
|
||||
label: Contributing 🙌🏽
|
||||
multiple: false
|
||||
options:
|
||||
- "Yes, I am willing to help answer this question in a PR"
|
||||
- "No, I am afraid I cannot help regarding this"
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
It takes a lot of work 🏋🏻♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
|
||||
|
||||
35
.github/PULL_REQUEST_TEMPLATE.md
vendored
35
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -16,26 +16,33 @@ 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 -->
|
||||
<!-- In your opinion, is this ready to be merged as soon as it's reviewed? -->
|
||||
|
||||
## 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 😊
|
||||
|
||||
-->
|
||||
|
||||
32
.github/workflows/build.yml
vendored
32
.github/workflows/build.yml
vendored
@@ -1,32 +0,0 @@
|
||||
# Simple check that the build is valid and no linting errors.
|
||||
# Currently is run as a seperate workflow as it's fast to fail.
|
||||
name: Lint/Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- next
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- next
|
||||
|
||||
jobs:
|
||||
lint-and-build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12, 14, 16]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Install dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
- run: npm run lint
|
||||
- run: npm run build
|
||||
68
.github/workflows/codeql-analysis.yml
vendored
68
.github/workflows/codeql-analysis.yml
vendored
@@ -1,67 +1,27 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
name: Code Analysis
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, beta, next ]
|
||||
branches: [main, beta, next]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: '43 17 * * 2'
|
||||
- cron: "43 17 * * 2"
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
name: Verify
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
language: ["javascript"]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
|
||||
57
.github/workflows/integration.yml
vendored
57
.github/workflows/integration.yml
vendored
@@ -1,57 +0,0 @@
|
||||
name: Integration Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- next
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
# Only run tests integration against Pull Requests from branches in
|
||||
# this repository. We do this as integration tests require access to
|
||||
# secrets in GitHub and they are not exposed to tests run against
|
||||
# forks (for security reasons), so integration test against
|
||||
# Pull Requests from external repos just fail and generate noise.
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
|
||||
# We use self-hosted runners as cloud based runnners (e.g. AWS, GPC)
|
||||
# fail due to IP Address checks done by providers, which enforce
|
||||
# CAPTCHA checks on login request from cloud compute IP addresses to
|
||||
# prevent abuse.
|
||||
runs-on: self-hosted
|
||||
|
||||
# Target time is under 5 minutes to run all tests. If it takes longer than
|
||||
# 10 minutes should look at running tests in parallel. No individual flow
|
||||
# should take longer than 5 minutes to build and run.
|
||||
timeout-minutes: 10
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12, 14, 16]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
|
||||
# Run tests (build library, build + start test app in Docker, run tests)
|
||||
- run: npm test
|
||||
# TODO Tests should exit out if env vars not set (currently hangs)
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
||||
NEXTAUTH_TWITTER_ID: ${{secrets.NEXTAUTH_TWITTER_ID}}
|
||||
NEXTAUTH_TWITTER_SECRET: ${{secrets.NEXTAUTH_TWITTER_SECRET}}
|
||||
NEXTAUTH_TWITTER_USERNAME: ${{secrets.NEXTAUTH_TWITTER_USERNAME}}
|
||||
NEXTAUTH_TWITTER_PASSWORD: ${{secrets.NEXTAUTH_TWITTER_PASSWORD}}
|
||||
NEXTAUTH_GITHUB_ID: ${{secrets.NEXTAUTH_GITHUB_ID}}
|
||||
NEXTAUTH_GITHUB_SECRET: ${{secrets.NEXTAUTH_GITHUB_SECRET}}
|
||||
NEXTAUTH_GITHUB_USERNAME: ${{secrets.NEXTAUTH_GITHUB_USERNAME}}
|
||||
NEXTAUTH_GITHUB_PASSWORD: ${{secrets.NEXTAUTH_GITHUB_PASSWORD}}
|
||||
12
.github/workflows/labeler.yml
vendored
12
.github/workflows/labeler.yml
vendored
@@ -1,11 +1,13 @@
|
||||
name: "Pull Request Labeler"
|
||||
name: PR Labeler
|
||||
|
||||
on:
|
||||
- pull_request_target
|
||||
- pull_request_target
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
name: Triage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@main
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
- uses: actions/labeler@main
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
68
.github/workflows/release.yml
vendored
68
.github/workflows/release.yml
vendored
@@ -1,4 +1,5 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -7,20 +8,69 @@ on:
|
||||
- "next"
|
||||
- "3.x"
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: "Release"
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
- name: Init
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Node.js
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14
|
||||
- name: Install dependencies
|
||||
node-version: 16
|
||||
- name: Dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
- run: npx semantic-release@17
|
||||
- name: Build
|
||||
run: npm run build
|
||||
- name: Run tests
|
||||
run: npm test -- --coverage --verbose && npm run test:types
|
||||
- name: Coverage
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
directory: ./coverage
|
||||
fail_ci_if_error: false
|
||||
release-branch:
|
||||
name: Publish branch
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
environment: Production
|
||||
steps:
|
||||
- name: Init
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 16
|
||||
- name: Dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
- name: Publish to npm and GitHub
|
||||
run: npx semantic-release@17
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
NPM_TOKEN: ${{secrets.NPM_TOKEN}}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
release-pr:
|
||||
name: Publish PR
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
environment: Preview
|
||||
steps:
|
||||
- name: Init
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 16
|
||||
- name: Dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
- name: Publish to npm
|
||||
run: |
|
||||
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> .npmrc
|
||||
npm run version:pr
|
||||
npm publish --access public --tag experimental
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
|
||||
27
.github/workflows/types.yml
vendored
27
.github/workflows/types.yml
vendored
@@ -1,27 +0,0 @@
|
||||
name: Types
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- next
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- next
|
||||
|
||||
jobs:
|
||||
lint-and-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- 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
|
||||
run: npm run test:types
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -58,4 +58,7 @@ app/yarn.lock
|
||||
/_work
|
||||
|
||||
# Prisma migrations
|
||||
/prisma/migrations
|
||||
/prisma/migrations
|
||||
|
||||
# Tests
|
||||
/coverage
|
||||
1
.husky/.gitignore
vendored
Normal file
1
.husky/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
_
|
||||
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx pretty-quick --staged
|
||||
@@ -11,43 +11,49 @@ Please raise any significant new functionality or breaking change an issue for d
|
||||
## For contributors
|
||||
|
||||
Anyone can be a contributor. Either you found a typo, or you have an awesome feature request you could implement, we encourage you to create a Pull Request.
|
||||
|
||||
### Pull Requests
|
||||
|
||||
* The latest changes are always in `main`, so please make your Pull Request against that branch.
|
||||
* Pull Requests should be raised for any change
|
||||
* Pull Requests need approval of a [core contributor](https://next-auth.js.org/contributors#core-team) before merging
|
||||
* We use ESLint/Prettier for linting/formatting, so please run `npm run lint:fix` before committing to make resolving conflicts easier (VSCode users, check out [this ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and [this Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) to fix lint and formatting issues in development)
|
||||
* We encourage you to test your changes, and if you have the opportunity, please make those tests part of the Pull Request
|
||||
* If you add new functionality, please provide the corresponding documentation as well and make it part of the Pull Request
|
||||
- The latest changes are always in `main`, so please make your Pull Request against that branch.
|
||||
- Pull Requests should be raised for any change
|
||||
- Pull Requests need approval of a [core contributor](https://next-auth.js.org/contributors#core-team) before merging
|
||||
- We use ESLint/Prettier for linting/formatting, so please run `npm run lint:fix` before committing to make resolving conflicts easier (VSCode users, check out [this ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and [this Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) to fix lint and formatting issues in development)
|
||||
- We encourage you to test your changes, and if you have the opportunity, please make those tests part of the Pull Request
|
||||
- If you add new functionality, please provide the corresponding documentation as well and make it part of the Pull Request
|
||||
|
||||
### Setting up local environment
|
||||
|
||||
A quick guide on how to setup *next-auth* locally to work on it and test out any changes:
|
||||
A quick guide on how to setup _next-auth_ locally to work on it and test out any changes:
|
||||
|
||||
The dev application requires you to use `npm@7`.
|
||||
|
||||
1. Clone the repo:
|
||||
|
||||
```sh
|
||||
git clone git@github.com:nextauthjs/next-auth.git
|
||||
cd next-auth
|
||||
```
|
||||
|
||||
2. Install packages:
|
||||
2. Install packages, set up the dev application:
|
||||
|
||||
```sh
|
||||
npm i && npm dev:setup
|
||||
npm run dev:setup
|
||||
```
|
||||
|
||||
3. Populate `.env.local`:
|
||||
|
||||
Copy `app/.env.local.example` to `app/.env.local`, and add your env variables for each provider you want to test.
|
||||
|
||||
Copy `app/.env.local.example` to `app/.env.local`, and add your env variables for each provider you want to test.
|
||||
|
||||
> NOTE: You can add any environment variables to .env.local that you would like to use in your dev app.
|
||||
> You can find the next-auth config under`app/pages/api/auth/[...nextauth].js`.
|
||||
|
||||
1. Start the dev application/server:
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Your dev application will be available on ```http://localhost:3000```
|
||||
Your dev application will be available on `http://localhost:3000`
|
||||
|
||||
That's it! 🎉
|
||||
|
||||
@@ -64,6 +70,7 @@ When running `npm run dev`, you start a Next.js dev server on `http://localhost:
|
||||
#### Providers
|
||||
|
||||
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)
|
||||
|
||||
@@ -73,57 +80,43 @@ You can look at the existing built-in providers for inspiration.
|
||||
|
||||
#### Databases
|
||||
|
||||
Included is a Docker Compose file that starts up MySQL, PostgreSQL, and MongoDB databases on localhost.
|
||||
|
||||
It will use port `3306`, `5432`, and `27017` on localhost respectively; please make sure those ports are not used by other services on localhost.
|
||||
|
||||
You can start them with `npm run db:start` and stop them with `npm run db:stop`.
|
||||
|
||||
You will need Docker and Docker Compose installed to be able to start / stop the databases.
|
||||
|
||||
When stopping the databases, it will reset their contents.
|
||||
|
||||
If you would like to contribute to an existing database adapter or help create a new one, head over to the [nextauthjs/adapters](https://www.github.com/nextauthjs/adapters) repository and follow the instructions provided there.
|
||||
#### Testing
|
||||
|
||||
Tests can be run with `npm run test`.
|
||||
|
||||
Automated tests are currently crude and limited in functionality, but improvements are in development.
|
||||
|
||||
Currently, to run tests you need to first have started local test databases (e.g. using `npm run db:start`).
|
||||
|
||||
The databases can take a few seconds to start up, so you might need to give it a minute before running the tests.
|
||||
|
||||
## For maintainers
|
||||
|
||||
We use [semantic-release](https://github.com/semantic-release/semantic-release) together with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0) to automate releases. This makes the maintainenance process easier and less error-prone to human error. Please study the "Conventional Commits" site to understand how to write a good commit message.
|
||||
We use [semantic-release](https://github.com/semantic-release/semantic-release) together with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0) to automate releases. This makes the maintenance process easier and less error-prone to human error. Please study the "Conventional Commits" site to understand how to write a good commit message.
|
||||
|
||||
When accepting Pull Requests, make sure the following:
|
||||
|
||||
* Use "Squash and merge"
|
||||
* Make sure you merge contributor PRs into `main`
|
||||
* Rewrite the commit message to conform to the `Conventional Commits` style. Check the "Recommended Scopes" section for further advice.
|
||||
* Optionally link issues the PR will resolve (You can add "close" in front of the issue numbers to close the issues automatically, when the PR is merged. `semantic-release` will also comment back to connected issues and PRs, notifying the users that a feature is added/bug fixed, etc.)
|
||||
- Use "Squash and merge"
|
||||
- Make sure you merge contributor PRs into `main`
|
||||
- Rewrite the commit message to conform to the `Conventional Commits` style. Check the "Recommended Scopes" section for further advice.
|
||||
- Optionally link issues the PR will resolve (You can add "close" in front of the issue numbers to close the issues automatically, when the PR is merged. `semantic-release` will also comment back to connected issues and PRs, notifying the users that a feature is added/bug fixed, etc.)
|
||||
|
||||
### Recommended Scopes
|
||||
|
||||
A typical conventional commit looks like this:
|
||||
|
||||
```
|
||||
type(scope): title
|
||||
|
||||
body
|
||||
```
|
||||
|
||||
Scope is the part that will help groupping the different commit types in the release notes.
|
||||
Scope is the part that will help grouping the different commit types in the release notes.
|
||||
|
||||
Some recommened scopes are:
|
||||
Some recommended scopes are:
|
||||
|
||||
- **provider** - Provider related changes. (eg.: "feat(provider): add X provider", "docs(provider): fix typo in X documentation"
|
||||
- **adapter** - Adapter related changes. (eg.: "feat(adapter): add X provider", "docs(provider): fix typo in X documentation"
|
||||
- **db** - Database related changes. (eg.: "feat(db): add X database", "docs(db): fix typo in X documentation"
|
||||
- **deps** - Adding/removing/updating a dependency (eg.: "chore(deps): add X")
|
||||
|
||||
> NOTE: If you are not sure which scope to use, you can simply ignore it. (eg.: "feat: add something"). Adding the correct type already helps a lot when analyzing the commit messages.
|
||||
|
||||
> NOTE: If you are not sure which scope to use, you can simply ignore it. (eg.: "feat: add something"). Adding the correct type already helps a lot when analyzing the commit messages.
|
||||
|
||||
### Skipping a release
|
||||
|
||||
|
||||
@@ -7,11 +7,8 @@
|
||||
Open Source. Full Stack. Own Your Data.
|
||||
</p>
|
||||
<p align="center" style="align: center;">
|
||||
<a href="https://github.com/nextauthjs/next-auth/actions?query=workflow%3ARelease">
|
||||
<img src="https://github.com/nextauthjs/next-auth/workflows/Release/badge.svg" alt="Release" />
|
||||
</a>
|
||||
<a href="https://github.com/nextauthjs/next-auth/actions?query=workflow%3A%22Integration+Test%22">
|
||||
<img src="https://github.com/nextauthjs/next-auth/workflows/Integration%20Test/badge.svg" alt="Integration Test" />
|
||||
<a href="https://github.com/nextauthjs/next-auth/actions/workflows/release.yml?query=workflow%3ARelease">
|
||||
<img src="https://github.com/nextauthjs/next-auth/actions/workflows/release.yml/badge.svg" alt="Release" />
|
||||
</a>
|
||||
<a href="https://bundlephobia.com/result?p=next-auth">
|
||||
<img src="https://img.shields.io/bundlephobia/minzip/next-auth" alt="Bundle Size"/>
|
||||
@@ -84,7 +81,7 @@ Advanced options allow you to define your own routines to handle controlling wha
|
||||
|
||||
### TypeScript
|
||||
|
||||
NextAuth.js comes with built-in types. For more information and usage, check out the [TypeScript section](https://next-auth.js.org/getting-started/typescript) in the documentaion.
|
||||
NextAuth.js comes with built-in types. For more information and usage, check out the [TypeScript section](https://next-auth.js.org/getting-started/typescript) in the documentation.
|
||||
|
||||
The package at `@types/next-auth` is now deprecated.
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ If you contact us regarding a serious issue:
|
||||
* We will endeavor to get back to you within 72 hours.
|
||||
* We will aim to publish a fix within 30 days.
|
||||
* We will disclose the issue (and credit you, with your consent) once a fix to resolve the issue has been released.
|
||||
* If 90 days has elapsed and we still don't have a fix, we will disclose the issue publically.
|
||||
* If 90 days has elapsed and we still don't have a fix, we will disclose the issue publicly.
|
||||
|
||||
Currently, the best way to report an issue is by emailing me@iaincollins.com
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
|
||||
# You can use `openssl rand -hex 32` or
|
||||
# https://generate-secret.now.sh/32 to generate a secret.
|
||||
# https://generate-secret.vercel.app/32 to generate a secret.
|
||||
# Note: Changing a secret may invalidate existing sessions
|
||||
# and/or verificaion tokens.
|
||||
SECRET=
|
||||
@@ -23,8 +23,6 @@ TWITTER_SECRET=
|
||||
EMAIL_SERVER=smtps://user@gmail.com:password@smtp.gmail.com:465
|
||||
EMAIL_FROM=user@gmail.com
|
||||
|
||||
# You can use any of these as the "DATABASE_URL" for
|
||||
# databases started with Docker using `npm run db:start`.
|
||||
# Note: If using with Prisma adapter, you need to use a `.env`
|
||||
# file rather than a `.env.local` file to configure env vars.
|
||||
# Postgres: DATABASE_URL=postgres://nextauth:password@127.0.0.1:5432/nextauth?synchronize=true
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
import Link from 'next/link'
|
||||
import styles from './footer.module.css'
|
||||
import { version } from 'package.json'
|
||||
import Link from "next/link"
|
||||
import styles from "./footer.module.css"
|
||||
import packageJSON from "package.json"
|
||||
|
||||
export default function Footer () {
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className={styles.footer}>
|
||||
<hr />
|
||||
<ul className={styles.navItems}>
|
||||
<li className={styles.navItem}><a href='https://next-auth.js.org'>Documentation</a></li>
|
||||
<li className={styles.navItem}><a href='https://www.npmjs.com/package/next-auth'>NPM</a></li>
|
||||
<li className={styles.navItem}><a href='https://github.com/nextauthjs/next-auth-example'>GitHub</a></li>
|
||||
<li className={styles.navItem}><Link href='/policy'><a>Policy</a></Link></li>
|
||||
<li className={styles.navItem}><em>{version}</em></li>
|
||||
<li className={styles.navItem}>
|
||||
<a href="https://next-auth.js.org">Documentation</a>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<a href="https://www.npmjs.com/package/next-auth">NPM</a>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<a href="https://github.com/nextauthjs/next-auth-example">GitHub</a>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href="/policy">
|
||||
<a>Policy</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<em>{packageJSON.version}</em>
|
||||
</li>
|
||||
</ul>
|
||||
</footer>
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
},
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"next": "^10.1.3",
|
||||
"next": "^11.0.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import NextAuth from 'next-auth'
|
||||
import Providers from 'next-auth/providers'
|
||||
import NextAuth from "next-auth"
|
||||
import EmailProvider from "next-auth/providers/email"
|
||||
import GitHubProvider from "next-auth/providers/github"
|
||||
import Auth0Provider from "next-auth/providers/auth0"
|
||||
import TwitterProvider from "next-auth/providers/twitter"
|
||||
import CredentialsProvider from "next-auth/providers/credentials"
|
||||
|
||||
// import Adapters from 'next-auth/adapters'
|
||||
// import { PrismaClient } from '@prisma/client'
|
||||
@@ -28,15 +32,15 @@ export default NextAuth({
|
||||
// }
|
||||
// },
|
||||
providers: [
|
||||
Providers.Email({
|
||||
EmailProvider({
|
||||
server: process.env.EMAIL_SERVER,
|
||||
from: process.env.EMAIL_FROM
|
||||
from: process.env.EMAIL_FROM,
|
||||
}),
|
||||
Providers.GitHub({
|
||||
GitHubProvider({
|
||||
clientId: process.env.GITHUB_ID,
|
||||
clientSecret: process.env.GITHUB_SECRET
|
||||
clientSecret: process.env.GITHUB_SECRET,
|
||||
}),
|
||||
Providers.Auth0({
|
||||
Auth0Provider({
|
||||
clientId: process.env.AUTH0_ID,
|
||||
clientSecret: process.env.AUTH0_SECRET,
|
||||
domain: process.env.AUTH0_DOMAIN,
|
||||
@@ -45,36 +49,36 @@ export default NextAuth({
|
||||
// authorizationParams: {
|
||||
// response_mode: 'form_post'
|
||||
// }
|
||||
protection: 'pkce'
|
||||
protection: "pkce",
|
||||
}),
|
||||
Providers.Twitter({
|
||||
TwitterProvider({
|
||||
clientId: process.env.TWITTER_ID,
|
||||
clientSecret: process.env.TWITTER_SECRET
|
||||
clientSecret: process.env.TWITTER_SECRET,
|
||||
}),
|
||||
Providers.Credentials({
|
||||
name: 'Credentials',
|
||||
CredentialsProvider({
|
||||
name: "Credentials",
|
||||
credentials: {
|
||||
password: { label: 'Password', type: 'password' }
|
||||
password: { label: "Password", type: "password" },
|
||||
},
|
||||
async authorize (credentials) {
|
||||
if (credentials.password === 'password') {
|
||||
async authorize(credentials, req) {
|
||||
if (credentials.password === "password") {
|
||||
return {
|
||||
id: 1,
|
||||
name: 'Fill Murray',
|
||||
email: 'bill@fillmurray.com',
|
||||
image: 'https://www.fillmurray.com/64/64'
|
||||
name: "Fill Murray",
|
||||
email: "bill@fillmurray.com",
|
||||
image: "https://www.fillmurray.com/64/64",
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
})
|
||||
},
|
||||
}),
|
||||
],
|
||||
jwt: {
|
||||
encryption: true,
|
||||
secret: process.env.SECRET
|
||||
secret: process.env.SECRET,
|
||||
},
|
||||
debug: false,
|
||||
theme: 'auto'
|
||||
theme: "auto",
|
||||
|
||||
// Default Database Adapter (TypeORM)
|
||||
// database: process.env.DATABASE_URL
|
||||
|
||||
@@ -4,6 +4,6 @@ import jwt from 'next-auth/jwt'
|
||||
const secret = process.env.SECRET
|
||||
|
||||
export default async (req, res) => {
|
||||
const token = await jwt.getToken({ req, secret })
|
||||
const token = await jwt.getToken({ req, secret, encryption: true })
|
||||
res.send(JSON.stringify(token, null, 2))
|
||||
}
|
||||
|
||||
33
config/babel.config.js
Normal file
33
config/babel.config.js
Normal file
@@ -0,0 +1,33 @@
|
||||
// We aim to have the same support as Next.js
|
||||
// https://nextjs.org/docs/getting-started#system-requirements
|
||||
// https://nextjs.org/docs/basic-features/supported-browsers-features
|
||||
|
||||
module.exports = {
|
||||
presets: [["@babel/preset-env", { targets: { node: "10.13" } }]],
|
||||
plugins: [
|
||||
"@babel/plugin-proposal-optional-catch-binding",
|
||||
"@babel/plugin-transform-runtime",
|
||||
],
|
||||
comments: false,
|
||||
overrides: [
|
||||
{
|
||||
test: ["../src/client/**"],
|
||||
presets: [["@babel/preset-env", { targets: { ie: "11" } }]],
|
||||
},
|
||||
{
|
||||
test: ["../src/server/pages/**"],
|
||||
presets: ["preact"],
|
||||
},
|
||||
{
|
||||
test: ["../src/**/*.test.js"],
|
||||
presets: [
|
||||
[
|
||||
"@babel/preset-react",
|
||||
{
|
||||
runtime: "automatic",
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
["@babel/preset-env", { "targets": { "esmodules": true } }]
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-class-properties"
|
||||
],
|
||||
"comments": false,
|
||||
"overrides": [
|
||||
{
|
||||
"test": ["../src/server/pages/**"],
|
||||
"presets": ["preact"]
|
||||
}
|
||||
]
|
||||
}
|
||||
2
config/jest-setup.js
Normal file
2
config/jest-setup.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import "@testing-library/jest-dom"
|
||||
import "whatwg-fetch"
|
||||
11
config/jest.config.js
Normal file
11
config/jest.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/** @type {import('@jest/types').Config.InitialOptions} */
|
||||
module.exports = {
|
||||
transform: {
|
||||
"\\.js$": ["babel-jest", { configFile: "./config/babel.config.js" }],
|
||||
},
|
||||
rootDir: "../src",
|
||||
setupFilesAfterEnv: ["../config/jest-setup.js"],
|
||||
collectCoverageFrom: ["!client/__tests__/**"],
|
||||
testMatch: ["**/*.test.js"],
|
||||
coverageDirectory: "../coverage",
|
||||
}
|
||||
17
config/version-pr.js
Normal file
17
config/version-pr.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const fs = require("fs-extra")
|
||||
const path = require("path")
|
||||
|
||||
try {
|
||||
const packageJSONPath = path.join(process.cwd(), "package.json")
|
||||
const packageJSON = JSON.parse(fs.readFileSync(packageJSONPath, "utf8"))
|
||||
|
||||
const sha8 = process.env.GITHUB_SHA.substr(0, 8)
|
||||
const prNumber = process.env.PR_NUMBER
|
||||
|
||||
packageJSON.version = `0.0.0-pr.${prNumber}.${sha8}`
|
||||
|
||||
fs.writeFileSync(packageJSONPath, JSON.stringify(packageJSON))
|
||||
} catch (error) {
|
||||
console.error("Could not set PR version", error)
|
||||
process.exit(1)
|
||||
}
|
||||
37767
package-lock.json
generated
37767
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
92
package.json
92
package.json
@@ -30,31 +30,20 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run build:js && npm run build:css",
|
||||
"build:js": "node ./config/build.js && babel --config-file ./config/babel.config.json src --out-dir dist",
|
||||
"build:js": "node ./config/build.js && babel --config-file ./config/babel.config.js src --out-dir dist",
|
||||
"build:css": "postcss --config config/postcss.config.js src/**/*.css --base src --dir dist && node config/wrap-css.js",
|
||||
"dev:setup": "npm run build:css && cd app && npm i",
|
||||
"dev:setup": "npm i && npm run build:css && cd app && npm i",
|
||||
"dev": "cd app && npm run dev",
|
||||
"watch": "npm run watch:js | npm run watch:css",
|
||||
"watch:js": "babel --config-file ./config/babel.config.json --watch src --out-dir dist",
|
||||
"watch:js": "babel --config-file ./config/babel.config.js --watch src --out-dir dist",
|
||||
"watch:css": "postcss --config config/postcss.config.js --watch src/**/*.css --base src --dir dist",
|
||||
"test: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:types": "dtslint types",
|
||||
"db:start": "docker-compose -f test/docker/databases.yml up -d",
|
||||
"db:stop": "docker-compose -f test/docker/databases.yml down",
|
||||
"test": "jest --config ./config/jest.config.js",
|
||||
"test:ci": "npm run lint && npm run test:types && npm run test -- --ci",
|
||||
"test:types": "dtslint types --onlyTestTsNext",
|
||||
"prepublishOnly": "npm run build",
|
||||
"publish:beta": "npm publish --tag beta",
|
||||
"publish:canary": "npm publish --tag canary",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix"
|
||||
"lint:fix": "eslint . --fix",
|
||||
"version:pr": "node ./config/version-pr"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
@@ -74,7 +63,9 @@
|
||||
],
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"crypto-js": "^4.0.0",
|
||||
"@babel/runtime": "^7.14.0",
|
||||
"@next-auth/prisma-legacy-adapter": "0.0.1-canary.127",
|
||||
"@next-auth/typeorm-legacy-adapter": "0.0.2-canary.129",
|
||||
"futoin-hkdf": "^1.3.2",
|
||||
"jose": "^1.27.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
@@ -83,13 +74,11 @@
|
||||
"pkce-challenge": "^2.1.0",
|
||||
"preact": "^10.4.1",
|
||||
"preact-render-to-string": "^5.1.14",
|
||||
"querystring": "^0.2.0",
|
||||
"require_optional": "^1.0.1",
|
||||
"typeorm": "^0.2.30"
|
||||
"querystring": "^0.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.13.1 || ^17",
|
||||
"react-dom": "16.13.1 || ^17"
|
||||
"react-dom": "^16.13.1 || ^17"
|
||||
},
|
||||
"peerOptionalDependencies": {
|
||||
"mongodb": "^3.5.9",
|
||||
@@ -101,17 +90,19 @@
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.8.4",
|
||||
"@babel/core": "^7.9.6",
|
||||
"@babel/plugin-proposal-class-properties": "^7.13.0",
|
||||
"@babel/plugin-proposal-optional-catch-binding": "^7.14.2",
|
||||
"@babel/plugin-transform-runtime": "^7.13.15",
|
||||
"@babel/preset-env": "^7.9.6",
|
||||
"@prisma/client": "^2.16.1",
|
||||
"@semantic-release/commit-analyzer": "^8.0.1",
|
||||
"@semantic-release/github": "^7.2.0",
|
||||
"@semantic-release/npm": "7.0.8",
|
||||
"@semantic-release/release-notes-generator": "^9.0.1",
|
||||
"@babel/preset-react": "^7.13.13",
|
||||
"@testing-library/jest-dom": "^5.12.0",
|
||||
"@testing-library/react": "^11.2.6",
|
||||
"@testing-library/user-event": "^13.1.9",
|
||||
"@types/nodemailer": "^6.4.2",
|
||||
"@types/react": "^17.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.22.0",
|
||||
"@typescript-eslint/parser": "^4.22.0",
|
||||
"autoprefixer": "^9.7.6",
|
||||
"babel-jest": "^26.6.3",
|
||||
"babel-preset-preact": "^2.0.0",
|
||||
"conventional-changelog-conventionalcommits": "4.4.0",
|
||||
"cssnano": "^4.1.10",
|
||||
@@ -121,25 +112,22 @@
|
||||
"eslint-config-prettier": "^8.2.0",
|
||||
"eslint-config-standard-with-typescript": "^19.0.1",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-jest": "^24.3.6",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.3.1",
|
||||
"eslint-plugin-standard": "^5.0.0",
|
||||
"mocha": "^8.1.3",
|
||||
"mongodb": "^3.5.9",
|
||||
"mssql": "^6.2.1",
|
||||
"mysql": "^2.18.1",
|
||||
"next": "^10.0.5",
|
||||
"pg": "^8.2.1",
|
||||
"husky": "^6.0.0",
|
||||
"jest": "^26.6.3",
|
||||
"msw": "^0.28.2",
|
||||
"next": "^11.0.1",
|
||||
"postcss-cli": "^7.1.1",
|
||||
"postcss-nested": "^4.2.1",
|
||||
"prettier": "^2.2.1",
|
||||
"prisma": "^2.16.1",
|
||||
"puppeteer": "^5.2.1",
|
||||
"puppeteer-extra": "^3.1.15",
|
||||
"puppeteer-extra-plugin-stealth": "^2.6.1",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"typescript": "^4.1.3"
|
||||
"pretty-quick": "^3.1.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"typescript": "^4.1.3",
|
||||
"whatwg-fetch": "^3.6.2"
|
||||
},
|
||||
"prettier": {
|
||||
"semi": false
|
||||
@@ -166,7 +154,23 @@
|
||||
"localStorage": "readonly",
|
||||
"location": "readonly",
|
||||
"fetch": "readonly"
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"./**/*test.js"
|
||||
],
|
||||
"env": {
|
||||
"jest/globals": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:jest/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"jest"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"release": {
|
||||
"branches": [
|
||||
|
||||
36
src/adapters/error-handler.js
Normal file
36
src/adapters/error-handler.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { UnknownError } from "../lib/errors"
|
||||
|
||||
/**
|
||||
* Handles adapter induced errors.
|
||||
* @param {import("types/adapters").AdapterInstance} adapter
|
||||
* @param {import("types").LoggerInstance} logger
|
||||
* @return {import("types/adapters").AdapterInstance}
|
||||
*/
|
||||
export default function adapterErrorHandler(adapter, logger) {
|
||||
return Object.keys(adapter).reduce((acc, method) => {
|
||||
const name = capitalize(method)
|
||||
const code = upperSnake(name, adapter.displayName)
|
||||
|
||||
const adapterMethod = adapter[method]
|
||||
acc[method] = async (...args) => {
|
||||
try {
|
||||
logger.debug(code, ...args)
|
||||
return await adapterMethod(...args)
|
||||
} catch (error) {
|
||||
logger.error(`${code}_ERROR`, error)
|
||||
const e = new UnknownError(error)
|
||||
e.name = `${name}Error`
|
||||
throw e
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
function capitalize(s) {
|
||||
return `${s[0].toUpperCase()}${s.slice(1)}`
|
||||
}
|
||||
|
||||
function upperSnake(s, prefix = "ADAPTER") {
|
||||
return `${prefix}_${s.replace(/([A-Z])/g, "_$1")}`.toUpperCase()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import TypeORM from './typeorm'
|
||||
import Prisma from './prisma'
|
||||
import * as TypeORM from "./typeorm"
|
||||
import * as Prisma from "./prisma"
|
||||
|
||||
export { TypeORM, Prisma }
|
||||
|
||||
export default {
|
||||
Default: TypeORM.Adapter,
|
||||
TypeORM,
|
||||
Prisma
|
||||
Prisma,
|
||||
}
|
||||
|
||||
6
src/adapters/prisma.js
Normal file
6
src/adapters/prisma.js
Normal file
@@ -0,0 +1,6 @@
|
||||
/*
|
||||
* Source code can be found at:
|
||||
* https://github.com/nextauthjs/adapters/tree/canary/packages/prisma-legacy
|
||||
*/
|
||||
|
||||
export { PrismaLegacyAdapter as Adapter } from "@next-auth/prisma-legacy-adapter"
|
||||
@@ -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
|
||||
}
|
||||
9
src/adapters/typeorm.js
Normal file
9
src/adapters/typeorm.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* Source code can be found at:
|
||||
* https://github.com/nextauthjs/adapters/tree/canary/packages/typeorm-legacy
|
||||
*/
|
||||
|
||||
export {
|
||||
TypeORMLegacyAdapter as Adapter,
|
||||
Models,
|
||||
} from "@next-auth/typeorm-legacy-adapter"
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
64
src/client/__tests__/client-provider.test.js
Normal file
64
src/client/__tests__/client-provider.test.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import { useState } from "react"
|
||||
import { rest } from "msw"
|
||||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
import { server, mockSession } from "./helpers/mocks"
|
||||
import { Provider, useSession } from ".."
|
||||
import userEvent from "@testing-library/user-event"
|
||||
|
||||
beforeAll(() => {
|
||||
server.listen()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
server.resetHandlers()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
server.close()
|
||||
})
|
||||
|
||||
test("fetches the session once and re-uses it for different consumers", async () => {
|
||||
const sessionRouteCall = jest.fn()
|
||||
|
||||
server.use(
|
||||
rest.get("/api/auth/session", (req, res, ctx) => {
|
||||
sessionRouteCall()
|
||||
res(ctx.status(200), ctx.json(mockSession))
|
||||
})
|
||||
)
|
||||
|
||||
render(<ProviderFlow />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(sessionRouteCall).toHaveBeenCalledTimes(1)
|
||||
|
||||
const session1 = screen.getByTestId("session-consumer-1").textContent
|
||||
const session2 = screen.getByTestId("session-consumer-2").textContent
|
||||
|
||||
expect(session1).toEqual(session2)
|
||||
})
|
||||
})
|
||||
|
||||
function ProviderFlow({ options = {} }) {
|
||||
return (
|
||||
<>
|
||||
<Provider options={options}>
|
||||
<SessionConsumer />
|
||||
<SessionConsumer testId="2" />
|
||||
</Provider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function SessionConsumer({ testId = 1 }) {
|
||||
const [session, loading] = useSession()
|
||||
|
||||
if (loading) return <span>loading</span>
|
||||
|
||||
return (
|
||||
<div data-testid={`session-consumer-${testId}`}>
|
||||
{JSON.stringify(session)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
105
src/client/__tests__/csrf.test.js
Normal file
105
src/client/__tests__/csrf.test.js
Normal file
@@ -0,0 +1,105 @@
|
||||
import { useState } from "react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
import { server, mockCSRFToken } from "./helpers/mocks"
|
||||
import logger from "../../lib/logger"
|
||||
import { getCsrfToken } from ".."
|
||||
import { rest } from "msw"
|
||||
|
||||
jest.mock("../../lib/logger", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
warn: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
proxyLogger(logger) {
|
||||
return logger
|
||||
},
|
||||
}))
|
||||
|
||||
beforeAll(() => {
|
||||
server.listen()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
server.close()
|
||||
})
|
||||
|
||||
test("returns the Cross Site Request Forgery Token (CSRF Token) required to make POST requests", async () => {
|
||||
render(<CSRFFlow />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("csrf-result").textContent).toEqual(
|
||||
mockCSRFToken.csrfToken
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test("when there's no CSRF token returned, it'll reflect that", async () => {
|
||||
server.use(
|
||||
rest.get("/api/auth/csrf", (req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
...mockCSRFToken,
|
||||
csrfToken: null,
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
render(<CSRFFlow />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("csrf-result").textContent).toBe("null-response")
|
||||
})
|
||||
})
|
||||
|
||||
test("when the fetch fails it'll throw a client fetch error", async () => {
|
||||
server.use(
|
||||
rest.get("/api/auth/csrf", (req, res, ctx) =>
|
||||
res(ctx.status(500), ctx.text("some error happened"))
|
||||
)
|
||||
)
|
||||
|
||||
render(<CSRFFlow />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(logger.error).toHaveBeenCalledTimes(1)
|
||||
expect(logger.error).toBeCalledWith(
|
||||
"CLIENT_FETCH_ERROR",
|
||||
"csrf",
|
||||
new SyntaxError("Unexpected token s in JSON at position 0")
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
function CSRFFlow() {
|
||||
const [response, setResponse] = useState()
|
||||
|
||||
async function handleCSRF() {
|
||||
const result = await getCsrfToken()
|
||||
setResponse(result)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<p data-testid="csrf-result">
|
||||
{response === null ? "null-response" : response || "no response"}
|
||||
</p>
|
||||
<button onClick={handleCSRF}>Get CSRF</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
90
src/client/__tests__/helpers/mocks.js
Normal file
90
src/client/__tests__/helpers/mocks.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import { setupServer } from "msw/node"
|
||||
import { rest } from "msw"
|
||||
import { randomBytes } from "crypto"
|
||||
|
||||
export const mockSession = {
|
||||
ok: true,
|
||||
user: {
|
||||
image: null,
|
||||
name: "John",
|
||||
email: "john@email.com",
|
||||
},
|
||||
expires: 123213139,
|
||||
}
|
||||
|
||||
export const mockProviders = {
|
||||
ok: true,
|
||||
github: {
|
||||
id: "github",
|
||||
name: "Github",
|
||||
type: "oauth",
|
||||
signinUrl: "path/to/signin",
|
||||
callbackUrl: "path/to/callback",
|
||||
},
|
||||
credentials: {
|
||||
id: "credentials",
|
||||
name: "Credentials",
|
||||
type: "credentials",
|
||||
authorize: null,
|
||||
credentials: null,
|
||||
},
|
||||
email: {
|
||||
id: "email",
|
||||
type: "email",
|
||||
name: "Email",
|
||||
},
|
||||
}
|
||||
|
||||
export const mockCSRFToken = {
|
||||
ok: true,
|
||||
csrfToken: randomBytes(32).toString("hex"),
|
||||
}
|
||||
|
||||
export const mockGithubResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
url: "https://path/to/github/url",
|
||||
}
|
||||
|
||||
export const mockCredentialsResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
url: "https://path/to/credentials/url",
|
||||
}
|
||||
|
||||
export const mockEmailResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
url: "https://path/to/email/url",
|
||||
}
|
||||
|
||||
export const mockSignOutResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
url: "https://path/to/signout/url",
|
||||
}
|
||||
|
||||
export const server = setupServer(
|
||||
rest.post("/api/auth/signout", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockSignOutResponse))
|
||||
),
|
||||
rest.get("/api/auth/session", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockSession))
|
||||
),
|
||||
rest.get("/api/auth/csrf", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockCSRFToken))
|
||||
),
|
||||
rest.get("/api/auth/providers", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockProviders))
|
||||
),
|
||||
rest.post("/api/auth/signin/github", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockGithubResponse))
|
||||
),
|
||||
rest.post("/api/auth/callback/credentials", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockCredentialsResponse))
|
||||
),
|
||||
rest.post("/api/auth/signin/email", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockEmailResponse))
|
||||
),
|
||||
rest.post("/api/auth/_log", (req, res, ctx) => res(ctx.status(200)))
|
||||
)
|
||||
8
src/client/__tests__/helpers/utils.js
Normal file
8
src/client/__tests__/helpers/utils.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export function getBroadcastEvents() {
|
||||
return window.localStorage.setItem.mock.calls
|
||||
.filter((call) => call[0] === "nextauth.message")
|
||||
.map(([eventName, value]) => {
|
||||
const { timestamp, ...rest } = JSON.parse(value)
|
||||
return { eventName, value: rest }
|
||||
})
|
||||
}
|
||||
85
src/client/__tests__/providers.test.js
Normal file
85
src/client/__tests__/providers.test.js
Normal file
@@ -0,0 +1,85 @@
|
||||
import { useState } from "react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
import { server, mockProviders } from "./helpers/mocks"
|
||||
import { getProviders } from ".."
|
||||
import logger from "../../lib/logger"
|
||||
import { rest } from "msw"
|
||||
|
||||
jest.mock("../../lib/logger", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
warn: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
proxyLogger(logger) {
|
||||
return logger
|
||||
},
|
||||
}))
|
||||
|
||||
beforeAll(() => {
|
||||
server.listen()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
server.close()
|
||||
})
|
||||
|
||||
test("when called it'll return the currently configured providers for sign in", async () => {
|
||||
render(<ProvidersFlow />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("providers-result").textContent).toEqual(
|
||||
JSON.stringify(mockProviders)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test("when failing to fetch the providers, it'll log the error", async () => {
|
||||
server.use(
|
||||
rest.get("/api/auth/providers", (req, res, ctx) =>
|
||||
res(ctx.status(500), ctx.text("some error happened"))
|
||||
)
|
||||
)
|
||||
|
||||
render(<ProvidersFlow />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(logger.error).toHaveBeenCalledTimes(1)
|
||||
expect(logger.error).toBeCalledWith(
|
||||
"CLIENT_FETCH_ERROR",
|
||||
"providers",
|
||||
new SyntaxError("Unexpected token s in JSON at position 0")
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
function ProvidersFlow() {
|
||||
const [response, setResponse] = useState()
|
||||
|
||||
async function handleGerProviders() {
|
||||
const result = await getProviders()
|
||||
setResponse(result)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<p data-testid="providers-result">
|
||||
{response === null
|
||||
? "null-response"
|
||||
: JSON.stringify(response) || "no response"}
|
||||
</p>
|
||||
<button onClick={handleGerProviders}>Get Providers</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
98
src/client/__tests__/session.test.js
Normal file
98
src/client/__tests__/session.test.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
import { rest } from "msw"
|
||||
import { server, mockSession } from "./helpers/mocks"
|
||||
import logger from "../../lib/logger"
|
||||
import { useState, useEffect } from "react"
|
||||
import { getSession } from ".."
|
||||
import { getBroadcastEvents } from "./helpers/utils"
|
||||
|
||||
jest.mock("../../lib/logger", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
warn: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
proxyLogger(logger) {
|
||||
return logger
|
||||
},
|
||||
}))
|
||||
|
||||
beforeAll(() => server.listen())
|
||||
|
||||
beforeEach(() => {
|
||||
// eslint-disable-next-line no-proto
|
||||
jest.spyOn(window.localStorage.__proto__, "setItem")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
server.close()
|
||||
})
|
||||
|
||||
test("if it can fetch the session, it should store it in `localStorage`", async () => {
|
||||
render(<SessionFlow />)
|
||||
|
||||
// In the start, there is no session
|
||||
const noSession = await screen.findByText("No session")
|
||||
expect(noSession).toBeInTheDocument()
|
||||
|
||||
// After we fetched the session, it should have been rendered by `<SessionFlow />`
|
||||
const session = await screen.findByText(new RegExp(mockSession.user.name))
|
||||
expect(session).toBeInTheDocument()
|
||||
|
||||
const broadcastCalls = getBroadcastEvents()
|
||||
const [broadcastedEvent] = broadcastCalls
|
||||
|
||||
expect(broadcastCalls).toHaveLength(1)
|
||||
expect(broadcastCalls).toHaveLength(1)
|
||||
expect(broadcastedEvent.eventName).toBe("nextauth.message")
|
||||
expect(broadcastedEvent.value).toStrictEqual({
|
||||
data: {
|
||||
trigger: "getSession",
|
||||
},
|
||||
event: "session",
|
||||
})
|
||||
})
|
||||
|
||||
test("if there's an error fetching the session, it should log it", async () => {
|
||||
server.use(
|
||||
rest.get("/api/auth/session", (req, res, ctx) => {
|
||||
return res(ctx.status(500), ctx.body("Server error"))
|
||||
})
|
||||
)
|
||||
|
||||
render(<SessionFlow />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(logger.error).toHaveBeenCalledTimes(1)
|
||||
expect(logger.error).toBeCalledWith(
|
||||
"CLIENT_FETCH_ERROR",
|
||||
"session",
|
||||
new SyntaxError("Unexpected token S in JSON at position 0")
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
function SessionFlow() {
|
||||
const [session, setSession] = useState(null)
|
||||
useEffect(() => {
|
||||
async function fetchUserSession() {
|
||||
try {
|
||||
const result = await getSession()
|
||||
setSession(result)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
fetchUserSession()
|
||||
}, [])
|
||||
|
||||
if (session) return <pre>{JSON.stringify(session, null, 2)}</pre>
|
||||
|
||||
return <p>No session</p>
|
||||
}
|
||||
290
src/client/__tests__/sign-in.test.js
Normal file
290
src/client/__tests__/sign-in.test.js
Normal file
@@ -0,0 +1,290 @@
|
||||
import { useState } from "react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
import logger from "../../lib/logger"
|
||||
import {
|
||||
server,
|
||||
mockCredentialsResponse,
|
||||
mockEmailResponse,
|
||||
mockGithubResponse,
|
||||
} from "./helpers/mocks"
|
||||
import { signIn } from ".."
|
||||
import { rest } from "msw"
|
||||
|
||||
const { location } = window
|
||||
|
||||
jest.mock("../../lib/logger", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
warn: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
proxyLogger(logger) {
|
||||
return logger
|
||||
},
|
||||
}))
|
||||
|
||||
beforeAll(() => {
|
||||
server.listen()
|
||||
delete window.location
|
||||
window.location = {
|
||||
...location,
|
||||
replace: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
server.resetHandlers()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
window.location = location
|
||||
server.close()
|
||||
})
|
||||
|
||||
const callbackUrl = "https://redirects/to"
|
||||
|
||||
test.each`
|
||||
provider | type
|
||||
${""} | ${"no"}
|
||||
${"foo"} | ${"unknown"}
|
||||
`(
|
||||
"if $type provider, it redirects to the default sign-in page",
|
||||
async ({ provider }) => {
|
||||
render(<SignInFlow providerId={provider} callbackUrl={callbackUrl} />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledTimes(1)
|
||||
expect(window.location.replace).toHaveBeenCalledWith(
|
||||
`/api/auth/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test.each`
|
||||
provider | type
|
||||
${""} | ${"no"}
|
||||
${"foo"} | ${"unknown"}
|
||||
`(
|
||||
"if $type provider supplied and no callback URL, redirects using the current location",
|
||||
async ({ provider }) => {
|
||||
render(<SignInFlow providerId={provider} />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledTimes(1)
|
||||
expect(window.location.replace).toHaveBeenCalledWith(
|
||||
`/api/auth/signin?callbackUrl=${encodeURIComponent(
|
||||
window.location.href
|
||||
)}`
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test.each`
|
||||
provider | mockUrl
|
||||
${`email`} | ${mockEmailResponse.url}
|
||||
${`credentials`} | ${mockCredentialsResponse.url}
|
||||
`(
|
||||
"$provider provider redirects if `redirect` is `true`",
|
||||
async ({ provider, mockUrl }) => {
|
||||
render(<SignInFlow providerId={provider} redirect={true} />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledTimes(1)
|
||||
expect(window.location.replace).toHaveBeenCalledWith(mockUrl)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test("redirection can't be stopped using an oauth provider", async () => {
|
||||
render(
|
||||
<SignInFlow
|
||||
providerId="github"
|
||||
callbackUrl={callbackUrl}
|
||||
redirect={false}
|
||||
/>
|
||||
)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledTimes(1)
|
||||
expect(window.location.replace).toHaveBeenCalledWith(mockGithubResponse.url)
|
||||
})
|
||||
})
|
||||
|
||||
test("redirection can be stopped using the 'credentials' provider", async () => {
|
||||
render(
|
||||
<SignInFlow
|
||||
providerId="credentials"
|
||||
callbackUrl={callbackUrl}
|
||||
redirect={false}
|
||||
/>
|
||||
)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).not.toHaveBeenCalledWith(
|
||||
mockCredentialsResponse.url
|
||||
)
|
||||
|
||||
expect(screen.getByTestId("signin-result").textContent).not.toBe(
|
||||
"no response"
|
||||
)
|
||||
})
|
||||
|
||||
// snapshot the expected return shape from `signIn`
|
||||
expect(JSON.parse(screen.getByTestId("signin-result").textContent))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": null,
|
||||
"ok": true,
|
||||
"status": 200,
|
||||
"url": "https://path/to/credentials/url",
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test("redirection can be stopped using the 'email' provider", async () => {
|
||||
render(
|
||||
<SignInFlow providerId="email" callbackUrl={callbackUrl} redirect={false} />
|
||||
)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).not.toHaveBeenCalledWith(
|
||||
mockEmailResponse.url
|
||||
)
|
||||
|
||||
expect(screen.getByTestId("signin-result").textContent).not.toBe(
|
||||
"no response"
|
||||
)
|
||||
})
|
||||
|
||||
// snapshot the expected return shape from `signIn` oauth
|
||||
expect(JSON.parse(screen.getByTestId("signin-result").textContent))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": null,
|
||||
"ok": true,
|
||||
"status": 200,
|
||||
"url": "https://path/to/email/url",
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test("if callback URL contains a hash we force a window reload when re-directing", async () => {
|
||||
const mockUrlWithHash = "https://path/to/email/url#foo-bar-baz"
|
||||
|
||||
server.use(
|
||||
rest.post("/api/auth/signin/email", (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
...mockEmailResponse,
|
||||
url: mockUrlWithHash,
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
render(<SignInFlow providerId="email" callbackUrl={mockUrlWithHash} />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledTimes(1)
|
||||
expect(window.location.replace).toHaveBeenCalledWith(mockUrlWithHash)
|
||||
// the browser will not refresh the page if the redirect URL contains a hash, hence we force it on the client, see #1289
|
||||
expect(window.location.reload).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
test("params are propagated to the signin URL when supplied", async () => {
|
||||
let matchedParams = ""
|
||||
const authParams = "foo=bar&bar=foo"
|
||||
|
||||
server.use(
|
||||
rest.post("/api/auth/signin/github", (req, res, ctx) => {
|
||||
matchedParams = req.url.search
|
||||
return res(ctx.status(200), ctx.json(mockGithubResponse))
|
||||
})
|
||||
)
|
||||
|
||||
render(<SignInFlow providerId="github" authorizationParams={authParams} />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(matchedParams).toEqual(`?${authParams}`)
|
||||
})
|
||||
})
|
||||
|
||||
test("when it fails to fetch the providers, it redirected back to signin page", async () => {
|
||||
const errorMsg = "Error when retrieving providers"
|
||||
|
||||
server.use(
|
||||
rest.get("/api/auth/providers", (req, res, ctx) =>
|
||||
res(ctx.status(500), ctx.json(errorMsg))
|
||||
)
|
||||
)
|
||||
|
||||
render(<SignInFlow providerId="github" />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledWith(`/api/auth/error`)
|
||||
|
||||
expect(logger.error).toHaveBeenCalledTimes(1)
|
||||
expect(logger.error).toBeCalledWith(
|
||||
"CLIENT_FETCH_ERROR",
|
||||
"providers",
|
||||
errorMsg
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
function SignInFlow({
|
||||
providerId,
|
||||
callbackUrl,
|
||||
redirect = true,
|
||||
authorizationParams = {},
|
||||
}) {
|
||||
const [response, setResponse] = useState(null)
|
||||
|
||||
async function handleSignIn() {
|
||||
const result = await signIn(
|
||||
providerId,
|
||||
{
|
||||
callbackUrl,
|
||||
redirect,
|
||||
},
|
||||
authorizationParams
|
||||
)
|
||||
|
||||
setResponse(result)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<p data-testid="signin-result">
|
||||
{response ? JSON.stringify(response) : "no response"}
|
||||
</p>
|
||||
<button onClick={handleSignIn}>Sign in</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
129
src/client/__tests__/sign-out.test.js
Normal file
129
src/client/__tests__/sign-out.test.js
Normal file
@@ -0,0 +1,129 @@
|
||||
import { useState } from "react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
import { server, mockSignOutResponse } from "./helpers/mocks"
|
||||
import { signOut } from ".."
|
||||
import { rest } from "msw"
|
||||
import { getBroadcastEvents } from "./helpers/utils"
|
||||
|
||||
const { location } = window
|
||||
|
||||
beforeAll(() => {
|
||||
server.listen()
|
||||
delete window.location
|
||||
window.location = {
|
||||
...location,
|
||||
replace: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
// eslint-disable-next-line no-proto
|
||||
jest.spyOn(window.localStorage.__proto__, "setItem")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
server.resetHandlers()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
window.location = location
|
||||
server.close()
|
||||
})
|
||||
|
||||
const callbackUrl = "https://redirects/to"
|
||||
|
||||
test("by default it redirects to the current URL if the server did not provide one", async () => {
|
||||
server.use(
|
||||
rest.post("/api/auth/signout", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ ...mockSignOutResponse, url: undefined }))
|
||||
)
|
||||
)
|
||||
|
||||
render(<SignOutFlow />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledTimes(1)
|
||||
expect(window.location.replace).toHaveBeenCalledWith(window.location.href)
|
||||
})
|
||||
})
|
||||
|
||||
test("it redirects to the URL allowed by the server", async () => {
|
||||
render(<SignOutFlow callbackUrl={callbackUrl} />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledTimes(1)
|
||||
expect(window.location.replace).toHaveBeenCalledWith(
|
||||
mockSignOutResponse.url
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test("if url contains a hash during redirection a page reload happens", async () => {
|
||||
const mockUrlWithHash = "https://path/to/email/url#foo-bar-baz"
|
||||
|
||||
server.use(
|
||||
rest.post("/api/auth/signout", (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
...mockSignOutResponse,
|
||||
url: mockUrlWithHash,
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
render(<SignOutFlow />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.reload).toHaveBeenCalledTimes(1)
|
||||
expect(window.location.replace).toHaveBeenCalledWith(mockUrlWithHash)
|
||||
})
|
||||
})
|
||||
|
||||
test("will broadcast the signout event to other tabs", async () => {
|
||||
render(<SignOutFlow />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
const broadcastCalls = getBroadcastEvents()
|
||||
const [broadcastedEvent] = broadcastCalls
|
||||
|
||||
expect(broadcastCalls).toHaveLength(1)
|
||||
expect(broadcastedEvent.eventName).toBe("nextauth.message")
|
||||
expect(broadcastedEvent.value).toStrictEqual({
|
||||
data: {
|
||||
trigger: "signout",
|
||||
},
|
||||
event: "session",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function SignOutFlow({ callbackUrl, redirect = true }) {
|
||||
const [response, setResponse] = useState(null)
|
||||
|
||||
async function handleSignOut() {
|
||||
const result = await signOut({ callbackUrl, redirect })
|
||||
setResponse(result)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<p data-testid="signout-result">
|
||||
{response ? JSON.stringify(response) : "no response"}
|
||||
</p>
|
||||
<button onClick={handleSignOut}>Sign out</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -8,9 +8,15 @@
|
||||
//
|
||||
// We use HTTP POST requests with CSRF Tokens to protect against CSRF attacks.
|
||||
|
||||
import { useState, useEffect, useContext, createContext, createElement } from 'react'
|
||||
import _logger, { proxyLogger } from '../lib/logger'
|
||||
import parseUrl from '../lib/parse-url'
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
useContext,
|
||||
createContext,
|
||||
createElement,
|
||||
} from "react"
|
||||
import _logger, { proxyLogger } from "../lib/logger"
|
||||
import parseUrl from "../lib/parse-url"
|
||||
|
||||
// This behaviour mirrors the default behaviour for getting the site name that
|
||||
// happens server side in server/index.js
|
||||
@@ -22,8 +28,14 @@ import parseUrl from '../lib/parse-url'
|
||||
const __NEXTAUTH = {
|
||||
baseUrl: parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL).baseUrl,
|
||||
basePath: parseUrl(process.env.NEXTAUTH_URL).basePath,
|
||||
baseUrlServer: parseUrl(process.env.NEXTAUTH_URL_INTERNAL || process.env.NEXTAUTH_URL || process.env.VERCEL_URL).baseUrl,
|
||||
basePathServer: parseUrl(process.env.NEXTAUTH_URL_INTERNAL || process.env.NEXTAUTH_URL).basePath,
|
||||
baseUrlServer: parseUrl(
|
||||
process.env.NEXTAUTH_URL_INTERNAL ||
|
||||
process.env.NEXTAUTH_URL ||
|
||||
process.env.VERCEL_URL
|
||||
).baseUrl,
|
||||
basePathServer: parseUrl(
|
||||
process.env.NEXTAUTH_URL_INTERNAL || process.env.NEXTAUTH_URL
|
||||
).basePath,
|
||||
keepAlive: 0,
|
||||
clientMaxAge: 0,
|
||||
// Properties starting with _ are used for tracking internal app state
|
||||
@@ -31,7 +43,7 @@ const __NEXTAUTH = {
|
||||
_clientSyncTimer: null,
|
||||
_eventListenersAdded: false,
|
||||
_clientSession: undefined,
|
||||
_getSession: () => {}
|
||||
_getSession: () => {},
|
||||
}
|
||||
|
||||
const logger = proxyLogger(_logger, __NEXTAUTH.basePath)
|
||||
@@ -39,7 +51,7 @@ const logger = proxyLogger(_logger, __NEXTAUTH.basePath)
|
||||
const broadcast = BroadcastChannel()
|
||||
|
||||
// Add event listners on load
|
||||
if (typeof window !== 'undefined' && !__NEXTAUTH._eventListenersAdded) {
|
||||
if (typeof window !== "undefined" && !__NEXTAUTH._eventListenersAdded) {
|
||||
__NEXTAUTH._eventListenersAdded = true
|
||||
// Listen for storage events and update session if event fired from
|
||||
// another window (but suppress firing another event to avoid a loop)
|
||||
@@ -50,26 +62,30 @@ if (typeof window !== 'undefined' && !__NEXTAUTH._eventListenersAdded) {
|
||||
// on how the session object is being used in the client; it is
|
||||
// more robust to have each window/tab fetch it's own copy of the
|
||||
// session object rather than share it across instances.
|
||||
broadcast.receive(() => __NEXTAUTH._getSession({ event: 'storage' }))
|
||||
broadcast.receive(() => __NEXTAUTH._getSession({ event: "storage" }))
|
||||
|
||||
// Listen for document visibility change events and
|
||||
// if visibility of the document changes, re-fetch the session.
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
!document.hidden && __NEXTAUTH._getSession({ event: 'visibilitychange' })
|
||||
}, false)
|
||||
document.addEventListener(
|
||||
"visibilitychange",
|
||||
() => {
|
||||
!document.hidden && __NEXTAUTH._getSession({ event: "visibilitychange" })
|
||||
},
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
// Context to store session data globally
|
||||
/** @type {import("types/internals/client").SessionContext} */
|
||||
const SessionContext = createContext()
|
||||
|
||||
export function useSession (session) {
|
||||
export function useSession(session) {
|
||||
const context = useContext(SessionContext)
|
||||
if (context) return context
|
||||
return _useSessionHook(session)
|
||||
}
|
||||
|
||||
function _useSessionHook (session) {
|
||||
function _useSessionHook(session) {
|
||||
const [data, setData] = useState(session)
|
||||
const [loading, setLoading] = useState(!data)
|
||||
|
||||
@@ -77,7 +93,7 @@ function _useSessionHook (session) {
|
||||
__NEXTAUTH._getSession = async ({ event = null } = {}) => {
|
||||
try {
|
||||
const triggredByEvent = event !== null
|
||||
const triggeredByStorageEvent = event === 'storage'
|
||||
const triggeredByStorageEvent = event === "storage"
|
||||
|
||||
const clientMaxAge = __NEXTAUTH.clientMaxAge
|
||||
const clientLastSync = parseInt(__NEXTAUTH._clientLastSync)
|
||||
@@ -98,14 +114,19 @@ function _useSessionHook (session) {
|
||||
// tab or window that will come through as a triggeredByStorageEvent
|
||||
// event and will skip this logic)
|
||||
return
|
||||
} else if (clientMaxAge > 0 && currentTime < (clientLastSync + clientMaxAge)) {
|
||||
} else if (
|
||||
clientMaxAge > 0 &&
|
||||
currentTime < clientLastSync + clientMaxAge
|
||||
) {
|
||||
// If the session freshness is within clientMaxAge then don't request
|
||||
// it again on this call (avoids too many invokations).
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (clientSession === undefined) { __NEXTAUTH._clientSession = null }
|
||||
if (clientSession === undefined) {
|
||||
__NEXTAUTH._clientSession = null
|
||||
}
|
||||
|
||||
// Update clientLastSync before making response to avoid repeated
|
||||
// invokations that would otherwise be triggered while we are still
|
||||
@@ -116,7 +137,7 @@ function _useSessionHook (session) {
|
||||
// tell getSession not to trigger an event when it calls to avoid an
|
||||
// infinate loop.
|
||||
const newClientSessionData = await getSession({
|
||||
triggerEvent: !triggeredByStorageEvent
|
||||
triggerEvent: !triggeredByStorageEvent,
|
||||
})
|
||||
|
||||
// Save session state internally, just so we can track that we've checked
|
||||
@@ -126,7 +147,7 @@ function _useSessionHook (session) {
|
||||
setData(newClientSessionData)
|
||||
setLoading(false)
|
||||
} catch (error) {
|
||||
logger.error('CLIENT_USE_SESSION_ERROR', error)
|
||||
logger.error("CLIENT_USE_SESSION_ERROR", error)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
@@ -137,114 +158,112 @@ function _useSessionHook (session) {
|
||||
return [data, loading]
|
||||
}
|
||||
|
||||
export async function getSession (ctx) {
|
||||
const session = await _fetchData('session', ctx)
|
||||
export async function getSession(ctx) {
|
||||
const session = await _fetchData("session", ctx)
|
||||
if (ctx?.triggerEvent ?? true) {
|
||||
broadcast.post({ event: 'session', data: { trigger: 'getSession' } })
|
||||
broadcast.post({ event: "session", data: { trigger: "getSession" } })
|
||||
}
|
||||
return session
|
||||
}
|
||||
|
||||
export async function getCsrfToken (ctx) {
|
||||
return (await _fetchData('csrf', ctx))?.csrfToken
|
||||
export async function getCsrfToken(ctx) {
|
||||
return (await _fetchData("csrf", ctx))?.csrfToken
|
||||
}
|
||||
|
||||
export async function getProviders () {
|
||||
return _fetchData('providers')
|
||||
export async function getProviders() {
|
||||
return await _fetchData("providers")
|
||||
}
|
||||
|
||||
export async function signIn (provider, options = {}, authorizationParams = {}) {
|
||||
const {
|
||||
callbackUrl = window.location,
|
||||
redirect = true
|
||||
} = options
|
||||
export async function signIn(provider, options = {}, authorizationParams = {}) {
|
||||
const { callbackUrl = window.location.href, redirect = true } = options
|
||||
|
||||
const baseUrl = _apiBaseUrl()
|
||||
const providers = await getProviders()
|
||||
|
||||
// Redirect to sign in page if no valid provider specified
|
||||
if (!(provider in providers)) {
|
||||
// If Provider not recognized, redirect to sign in page
|
||||
window.location = `${baseUrl}/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
|
||||
return
|
||||
if (!providers) {
|
||||
return window.location.replace(`${baseUrl}/error`)
|
||||
}
|
||||
const isCredentials = providers[provider].type === 'credentials'
|
||||
const isEmail = providers[provider].type === 'email'
|
||||
const canRedirectBeDisabled = isCredentials || isEmail
|
||||
|
||||
if (!(provider in providers)) {
|
||||
return window.location.replace(
|
||||
`${baseUrl}/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
|
||||
)
|
||||
}
|
||||
|
||||
const isCredentials = providers[provider].type === "credentials"
|
||||
const isEmail = providers[provider].type === "email"
|
||||
const isSupportingReturn = isCredentials || isEmail
|
||||
|
||||
const signInUrl = isCredentials
|
||||
? `${baseUrl}/callback/${provider}`
|
||||
: `${baseUrl}/signin/${provider}`
|
||||
|
||||
// If is any other provider type, POST to provider URL with CSRF Token,
|
||||
// callback URL and any other parameters supplied.
|
||||
const fetchOptions = {
|
||||
method: 'post',
|
||||
const _signInUrl = `${signInUrl}?${new URLSearchParams(authorizationParams)}`
|
||||
|
||||
const res = await fetch(_signInUrl, {
|
||||
method: "post",
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
...options,
|
||||
csrfToken: await getCsrfToken(),
|
||||
callbackUrl,
|
||||
json: true
|
||||
})
|
||||
}
|
||||
const _signInUrl = `${signInUrl}?${new URLSearchParams(authorizationParams)}`
|
||||
const res = await fetch(_signInUrl, fetchOptions)
|
||||
const data = await res.json()
|
||||
if (redirect || !canRedirectBeDisabled) {
|
||||
const url = data.url ?? callbackUrl
|
||||
window.location = url
|
||||
// If url contains a hash, the browser does not reload the page. We reload manually
|
||||
if (url.includes('#')) window.location.reload()
|
||||
json: true,
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
|
||||
if (redirect || !isSupportingReturn) {
|
||||
const url = data.url ?? callbackUrl
|
||||
window.location.replace(url)
|
||||
// If url contains a hash, the browser does not reload the page. We reload manually
|
||||
if (url.includes("#")) window.location.reload()
|
||||
return
|
||||
}
|
||||
|
||||
const error = new URL(data.url).searchParams.get('error')
|
||||
const error = new URL(data.url).searchParams.get("error")
|
||||
|
||||
if (res.ok) {
|
||||
await __NEXTAUTH._getSession({ event: 'storage' })
|
||||
await __NEXTAUTH._getSession({ event: "storage" })
|
||||
}
|
||||
|
||||
return {
|
||||
error,
|
||||
status: res.status,
|
||||
ok: res.ok,
|
||||
url: error ? null : data.url
|
||||
url: error ? null : data.url,
|
||||
}
|
||||
}
|
||||
|
||||
export async function signOut (options = {}) {
|
||||
const {
|
||||
callbackUrl = window.location,
|
||||
redirect = true
|
||||
} = options
|
||||
export async function signOut(options = {}) {
|
||||
const { callbackUrl = window.location.href, redirect = true } = options
|
||||
const baseUrl = _apiBaseUrl()
|
||||
const fetchOptions = {
|
||||
method: 'post',
|
||||
method: "post",
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
csrfToken: await getCsrfToken(),
|
||||
callbackUrl,
|
||||
json: true
|
||||
})
|
||||
json: true,
|
||||
}),
|
||||
}
|
||||
const res = await fetch(`${baseUrl}/signout`, fetchOptions)
|
||||
const data = await res.json()
|
||||
broadcast.post({ event: 'session', data: { trigger: 'signout' } })
|
||||
broadcast.post({ event: "session", data: { trigger: "signout" } })
|
||||
|
||||
if (redirect) {
|
||||
const url = data.url ?? callbackUrl
|
||||
window.location = url
|
||||
window.location.replace(url)
|
||||
// If url contains a hash, the browser does not reload the page. We reload manually
|
||||
if (url.includes('#')) window.location.reload()
|
||||
if (url.includes("#")) window.location.reload()
|
||||
return
|
||||
}
|
||||
|
||||
await __NEXTAUTH._getSession({ event: 'storage' })
|
||||
await __NEXTAUTH._getSession({ event: "storage" })
|
||||
|
||||
return data
|
||||
}
|
||||
@@ -252,13 +271,18 @@ export async function signOut (options = {}) {
|
||||
// Method to set options. The documented way is to use the provider, but this
|
||||
// method is being left in as an alternative, that will be helpful if/when we
|
||||
// expose a vanilla JavaScript version that doesn't depend on React.
|
||||
export function setOptions ({ baseUrl, basePath, clientMaxAge, keepAlive } = {}) {
|
||||
export function setOptions({
|
||||
baseUrl,
|
||||
basePath,
|
||||
clientMaxAge,
|
||||
keepAlive,
|
||||
} = {}) {
|
||||
if (baseUrl) __NEXTAUTH.baseUrl = baseUrl
|
||||
if (basePath) __NEXTAUTH.basePath = basePath
|
||||
if (clientMaxAge) __NEXTAUTH.clientMaxAge = clientMaxAge
|
||||
if (keepAlive) {
|
||||
__NEXTAUTH.keepAlive = keepAlive
|
||||
if (typeof window === 'undefined') return
|
||||
if (typeof window === "undefined") return
|
||||
|
||||
// Clear existing timer (if there is one)
|
||||
if (__NEXTAUTH._clientSyncTimer !== null) {
|
||||
@@ -269,12 +293,12 @@ export function setOptions ({ baseUrl, basePath, clientMaxAge, keepAlive } = {})
|
||||
__NEXTAUTH._clientSyncTimer = setTimeout(async () => {
|
||||
// Only invoke keepalive when a session exists
|
||||
if (!__NEXTAUTH._clientSession) return
|
||||
await __NEXTAUTH._getSession({ event: 'timer' })
|
||||
await __NEXTAUTH._getSession({ event: "timer" })
|
||||
}, keepAlive * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
export function Provider ({ children, session, options }) {
|
||||
export function Provider({ children, session, options }) {
|
||||
setOptions(options)
|
||||
return createElement(
|
||||
SessionContext.Provider,
|
||||
@@ -290,24 +314,25 @@ export function Provider ({ children, session, options }) {
|
||||
* work seemlessly in getInitialProps() on server side
|
||||
* pages *and* in _app.js.
|
||||
*/
|
||||
async function _fetchData (path, { ctx, req = ctx?.req } = {}) {
|
||||
async function _fetchData(path, { ctx, req = ctx?.req } = {}) {
|
||||
try {
|
||||
const baseUrl = await _apiBaseUrl()
|
||||
const options = req ? { headers: { cookie: req.headers.cookie } } : {}
|
||||
const res = await fetch(`${baseUrl}/${path}`, options)
|
||||
const data = await res.json()
|
||||
if (!res.ok) throw data
|
||||
return Object.keys(data).length > 0 ? data : null // Return null if data empty
|
||||
} catch (error) {
|
||||
logger.error('CLIENT_FETCH_ERROR', path, error)
|
||||
logger.error("CLIENT_FETCH_ERROR", path, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function _apiBaseUrl () {
|
||||
if (typeof window === 'undefined') {
|
||||
function _apiBaseUrl() {
|
||||
if (typeof window === "undefined") {
|
||||
// NEXTAUTH_URL should always be set explicitly to support server side calls - log warning if not set
|
||||
if (!process.env.NEXTAUTH_URL) {
|
||||
logger.warn('NEXTAUTH_URL', 'NEXTAUTH_URL environment variable not set')
|
||||
logger.warn("NEXTAUTH_URL", "NEXTAUTH_URL environment variable not set")
|
||||
}
|
||||
|
||||
// Return absolute path when called server side
|
||||
@@ -318,7 +343,7 @@ function _apiBaseUrl () {
|
||||
}
|
||||
|
||||
/** Returns the number of seconds elapsed since January 1, 1970 00:00:00 UTC. */
|
||||
function _now () {
|
||||
function _now() {
|
||||
return Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
@@ -328,33 +353,48 @@ function _now () {
|
||||
*
|
||||
* https://caniuse.com/?search=broadcastchannel
|
||||
*/
|
||||
function BroadcastChannel (name = 'nextauth.message') {
|
||||
function BroadcastChannel(name = "nextauth.message") {
|
||||
return {
|
||||
/**
|
||||
* Get notified by other tabs/windows.
|
||||
* @param {(message: import("types/internals/client").BroadcastMessage) => void} onReceive
|
||||
*/
|
||||
receive (onReceive) {
|
||||
if (typeof window === 'undefined') return
|
||||
window.addEventListener('storage', async (event) => {
|
||||
receive(onReceive) {
|
||||
if (typeof window === "undefined") return
|
||||
window.addEventListener("storage", async (event) => {
|
||||
if (event.key !== name) return
|
||||
/** @type {import("types/internals/client").BroadcastMessage} */
|
||||
const message = JSON.parse(event.newValue)
|
||||
if (message?.event !== 'session' || !message?.data) return
|
||||
if (message?.event !== "session" || !message?.data) return
|
||||
|
||||
onReceive(message)
|
||||
})
|
||||
},
|
||||
/** Notify other tabs/windows. */
|
||||
post (message) {
|
||||
if (typeof localStorage === 'undefined') return
|
||||
localStorage.setItem(name,
|
||||
post(message) {
|
||||
if (typeof localStorage === "undefined") return
|
||||
localStorage.setItem(
|
||||
name,
|
||||
JSON.stringify({ ...message, timestamp: _now() })
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
@@ -374,5 +414,5 @@ export default {
|
||||
providers: getProviders,
|
||||
csrfToken: getCsrfToken,
|
||||
signin: signIn,
|
||||
signout: signOut
|
||||
signout: signOut,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
24
src/providers/coinbase.js
Normal file
24
src/providers/coinbase.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export default function Coinbase(options) {
|
||||
return {
|
||||
id: "coinbase",
|
||||
name: "Coinbase",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "wallet:user:email wallet:user:read",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://api.coinbase.com/oauth/token",
|
||||
requestTokenUrl: "https://api.coinbase.com/oauth/token",
|
||||
authorizationUrl:
|
||||
"https://www.coinbase.com/oauth/authorize?response_type=code",
|
||||
profileUrl: "https://api.coinbase.com/v2/user",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.data.id,
|
||||
name: profile.data.name,
|
||||
email: profile.data.email,
|
||||
image: profile.data.avatar_url,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
}
|
||||
54
src/providers/dropbox.js
Normal file
54
src/providers/dropbox.js
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* @param {import("../server").Provider} options
|
||||
* @example
|
||||
*
|
||||
* ```js
|
||||
* // pages/api/auth/[...nextauth].js
|
||||
* import Providers from `next-auth/providers`
|
||||
* ...
|
||||
* providers: [
|
||||
* Providers.Dropbox({
|
||||
* clientId: process.env.DROPBOX_CLIENT_ID,
|
||||
* clientSecret: process.env.DROPBOX_CLIENT_SECRET
|
||||
* })
|
||||
* ]
|
||||
* ...
|
||||
*
|
||||
* // pages/index
|
||||
* import { signIn } from "next-auth/client"
|
||||
* ...
|
||||
* <button onClick={() => signIn("dropbox")}>
|
||||
* Sign in
|
||||
* </button>
|
||||
* ...
|
||||
* ```
|
||||
* *Resources:*
|
||||
* - [NextAuth.js Documentation](https://next-auth.js.org/providers/dropbox)
|
||||
* - [Dropbox Documentation](https://developers.dropbox.com/oauth-guide)
|
||||
* - [Configuration](https://www.dropbox.com/developers/apps)
|
||||
*/
|
||||
export default function Dropbox(options) {
|
||||
return {
|
||||
id: 'dropbox',
|
||||
name: 'Dropbox',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: 'account_info.read',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: 'https://api.dropboxapi.com/oauth2/token',
|
||||
authorizationUrl:
|
||||
'https://www.dropbox.com/oauth2/authorize?token_access_type=offline&response_type=code',
|
||||
profileUrl: 'https://api.dropboxapi.com/2/users/get_current_account',
|
||||
profile: (profile) => {
|
||||
return {
|
||||
id: profile.account_id,
|
||||
name: profile.name.display_name,
|
||||
email: profile.email,
|
||||
image: profile.profile_photo_url,
|
||||
email_verified: profile.email_verified
|
||||
}
|
||||
},
|
||||
protection: ["state", "pkce"],
|
||||
...options
|
||||
}
|
||||
}
|
||||
18
src/providers/naver.js
Normal file
18
src/providers/naver.js
Normal file
@@ -0,0 +1,18 @@
|
||||
export default function Naver(options) {
|
||||
return {
|
||||
id: "naver",
|
||||
name: "Naver",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
params: { grant_type: "authorization_code" },
|
||||
protection: ["state"],
|
||||
accessTokenUrl: "https://nid.naver.com/oauth2.0/token",
|
||||
authorizationUrl:
|
||||
"https://nid.naver.com/oauth2.0/authorize?response_type=code",
|
||||
profileUrl: "https://openapi.naver.com/v1/nid/me",
|
||||
profile(profile) {
|
||||
return profile.response
|
||||
},
|
||||
...options,
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export default function Twitter(options) {
|
||||
id: profile.id_str,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: profile.profile_image_url_https.replace(/_normal\.jpg$/, ".jpg"),
|
||||
image: profile.profile_image_url_https.replace(/_normal\.(jpg|png|gif)$/, ".$1"),
|
||||
}
|
||||
},
|
||||
...options,
|
||||
|
||||
26
src/providers/workos.js
Normal file
26
src/providers/workos.js
Normal file
@@ -0,0 +1,26 @@
|
||||
export default function WorkOS(options) {
|
||||
const domain = options.domain || 'api.workos.com';
|
||||
|
||||
return {
|
||||
id: 'workos',
|
||||
name: 'WorkOS',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: '',
|
||||
params: {
|
||||
grant_type: 'authorization_code',
|
||||
client_id: options.clientId,
|
||||
client_secret: options.clientSecret
|
||||
},
|
||||
accessTokenUrl: `https://${domain}/sso/token`,
|
||||
authorizationUrl: `https://${domain}/sso/authorize?response_type=code`,
|
||||
profileUrl: `https://${domain}/sso/profile`,
|
||||
profile: (profile) => {
|
||||
return {
|
||||
...profile,
|
||||
name: `${profile.first_name} ${profile.last_name}`
|
||||
}
|
||||
},
|
||||
...options
|
||||
}
|
||||
}
|
||||
20
src/providers/zoom.js
Normal file
20
src/providers/zoom.js
Normal file
@@ -0,0 +1,20 @@
|
||||
export default function Zoom(options) {
|
||||
return {
|
||||
id: "zoom",
|
||||
name: "Zoom",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://zoom.us/oauth/token",
|
||||
authorizationUrl: "https://zoom.us/oauth/authorize?response_type=code",
|
||||
profileUrl: "https://api.zoom.us/v2/users/me",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: `${profile.first_name} ${profile.last_name}`,
|
||||
email: profile.email,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,24 @@
|
||||
import adapters from '../adapters'
|
||||
import jwt from '../lib/jwt'
|
||||
import parseUrl from '../lib/parse-url'
|
||||
import logger, { setLogger } from '../lib/logger'
|
||||
import * as cookie from './lib/cookie'
|
||||
import * as defaultEvents from './lib/default-events'
|
||||
import * as defaultCallbacks from './lib/default-callbacks'
|
||||
import parseProviders from './lib/providers'
|
||||
import * as routes from './routes'
|
||||
import renderPage from './pages'
|
||||
import createSecret from './lib/create-secret'
|
||||
import callbackUrlHandler from './lib/callback-url-handler'
|
||||
import extendRes from './lib/extend-res'
|
||||
import csrfTokenHandler from './lib/csrf-token-handler'
|
||||
import * as pkce from './lib/oauth/pkce-handler'
|
||||
import * as state from './lib/oauth/state-handler'
|
||||
import adapters from "../adapters"
|
||||
import jwt from "../lib/jwt"
|
||||
import parseUrl from "../lib/parse-url"
|
||||
import logger, { setLogger } from "../lib/logger"
|
||||
import * as cookie from "./lib/cookie"
|
||||
import * as defaultEvents from "./lib/default-events"
|
||||
import * as defaultCallbacks from "./lib/default-callbacks"
|
||||
import parseProviders from "./lib/providers"
|
||||
import * as routes from "./routes"
|
||||
import renderPage from "./pages"
|
||||
import createSecret from "./lib/create-secret"
|
||||
import callbackUrlHandler from "./lib/callback-url-handler"
|
||||
import extendRes from "./lib/extend-res"
|
||||
import csrfTokenHandler from "./lib/csrf-token-handler"
|
||||
import * as pkce from "./lib/oauth/pkce-handler"
|
||||
import * as state from "./lib/oauth/state-handler"
|
||||
|
||||
// To work properly in production with OAuth providers the NEXTAUTH_URL
|
||||
// environment variable must be set.
|
||||
if (!process.env.NEXTAUTH_URL) {
|
||||
logger.warn('NEXTAUTH_URL', 'NEXTAUTH_URL environment variable not set')
|
||||
logger.warn("NEXTAUTH_URL", "NEXTAUTH_URL environment variable not set")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,7 +26,7 @@ if (!process.env.NEXTAUTH_URL) {
|
||||
* @param {import("next").NextApiResponse} res
|
||||
* @param {import("types").NextAuthOptions} userOptions
|
||||
*/
|
||||
async function NextAuthHandler (req, res, userOptions) {
|
||||
async function NextAuthHandler(req, res, userOptions) {
|
||||
if (userOptions.logger) {
|
||||
setLogger(userOptions.logger)
|
||||
}
|
||||
@@ -39,13 +39,15 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
// to avoid early termination of calls to the serverless function
|
||||
// (and then return that promise when we are done) - eslint
|
||||
// complains but I'm not sure there is another way to do this.
|
||||
return new Promise(async resolve => { // eslint-disable-line no-async-promise-executor
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
return new Promise(async (resolve) => {
|
||||
extendRes(req, res, resolve)
|
||||
|
||||
if (!req.query.nextauth) {
|
||||
const error = 'Cannot find [...nextauth].js in pages/api/auth. Make sure the filename is written correctly.'
|
||||
const error =
|
||||
"Cannot find [...nextauth].js in pages/api/auth. Make sure the filename is written correctly."
|
||||
|
||||
logger.error('MISSING_NEXTAUTH_API_ROUTE_ERROR', error)
|
||||
logger.error("MISSING_NEXTAUTH_API_ROUTE_ERROR", error)
|
||||
return res.status(500).end(`Error: ${error}`)
|
||||
}
|
||||
|
||||
@@ -53,31 +55,48 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
nextauth,
|
||||
action = nextauth[0],
|
||||
providerId = nextauth[1],
|
||||
error = nextauth[1]
|
||||
error = nextauth[1],
|
||||
} = req.query
|
||||
|
||||
// @todo refactor all existing references to baseUrl and basePath
|
||||
const { basePath, baseUrl } = parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL)
|
||||
const { basePath, baseUrl } = parseUrl(
|
||||
process.env.NEXTAUTH_URL || process.env.VERCEL_URL
|
||||
)
|
||||
|
||||
const cookies = {
|
||||
...cookie.defaultCookies(userOptions.useSecureCookies || baseUrl.startsWith('https://')),
|
||||
...cookie.defaultCookies(
|
||||
userOptions.useSecureCookies || baseUrl.startsWith("https://")
|
||||
),
|
||||
// Allow user cookie options to override any cookie settings above
|
||||
...userOptions.cookies
|
||||
...userOptions.cookies,
|
||||
}
|
||||
|
||||
const secret = createSecret({ userOptions, basePath, baseUrl })
|
||||
|
||||
const providers = parseProviders({ providers: userOptions.providers, baseUrl, basePath })
|
||||
const providers = parseProviders({
|
||||
providers: userOptions.providers,
|
||||
baseUrl,
|
||||
basePath,
|
||||
})
|
||||
const provider = providers.find(({ id }) => id === providerId)
|
||||
|
||||
// Protection only works on OAuth 2.x providers
|
||||
if (provider?.type === 'oauth' && provider.version?.startsWith('2')) {
|
||||
// When provider.state is undefined, we still want this to pass
|
||||
if (!provider.protection && provider.state !== false) {
|
||||
// Default to state, as we did in 3.1 REVIEW: should we use "pkce" or "none" as default?
|
||||
provider.protection = ['state']
|
||||
} else if (typeof provider.protection === 'string') {
|
||||
provider.protection = [provider.protection]
|
||||
// TODO:
|
||||
// - rename to `checks` in 4.x, so it is similar to `openid-client`
|
||||
// - stop supporting `protection` as string
|
||||
// - remove `state` property
|
||||
if (provider?.type === "oauth" && provider.version?.startsWith("2")) {
|
||||
// Priority: (protection array > protection string) > state > default
|
||||
if (provider.protection) {
|
||||
provider.protection = Array.isArray(provider.protection)
|
||||
? provider.protection
|
||||
: [provider.protection]
|
||||
} else if (provider.state !== undefined) {
|
||||
provider.protection = [provider.state ? "state" : "none"]
|
||||
} else {
|
||||
// Default to state, as we did in 3.1
|
||||
// REVIEW: should we use "pkce" or "none" as default?
|
||||
provider.protection = ["state"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,14 +105,16 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
// Parse database / adapter
|
||||
// If adapter is provided, use it (advanced usage, overrides database)
|
||||
// If database URI or config object is provided, use it (simple usage)
|
||||
const adapter = userOptions.adapter ?? (userOptions.database && adapters.Default(userOptions.database))
|
||||
const adapter =
|
||||
userOptions.adapter ??
|
||||
(userOptions.database && adapters.Default(userOptions.database))
|
||||
|
||||
// User provided options are overriden by other options,
|
||||
// except for the options with special handling above
|
||||
req.options = {
|
||||
debug: false,
|
||||
pages: {},
|
||||
theme: 'auto',
|
||||
theme: "auto",
|
||||
// Custom options override defaults
|
||||
...userOptions,
|
||||
// These computed settings can have values in userOptions but we override them
|
||||
@@ -111,7 +132,7 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
jwt: !adapter, // If no adapter specified, force use of JSON Web Tokens (stateless)
|
||||
maxAge,
|
||||
updateAge: 24 * 60 * 60, // Sessions updated only if session is greater than this value (0 = always, 24*60*60 = every 24 hours)
|
||||
...userOptions.session
|
||||
...userOptions.session,
|
||||
},
|
||||
// JWT options
|
||||
jwt: {
|
||||
@@ -119,20 +140,20 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
maxAge, // same as session maxAge,
|
||||
encode: jwt.encode,
|
||||
decode: jwt.decode,
|
||||
...userOptions.jwt
|
||||
...userOptions.jwt,
|
||||
},
|
||||
// Event messages
|
||||
events: {
|
||||
...defaultEvents,
|
||||
...userOptions.events
|
||||
...userOptions.events,
|
||||
},
|
||||
// Callback functions
|
||||
callbacks: {
|
||||
...defaultCallbacks,
|
||||
...userOptions.callbacks
|
||||
...userOptions.callbacks,
|
||||
},
|
||||
pkce: {},
|
||||
logger
|
||||
logger,
|
||||
}
|
||||
|
||||
csrfTokenHandler(req, res)
|
||||
@@ -141,65 +162,74 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
const render = renderPage(req, res)
|
||||
const { pages } = req.options
|
||||
|
||||
if (req.method === 'GET') {
|
||||
if (req.method === "GET") {
|
||||
switch (action) {
|
||||
case 'providers':
|
||||
case "providers":
|
||||
return routes.providers(req, res)
|
||||
case 'session':
|
||||
case "session":
|
||||
return routes.session(req, res)
|
||||
case 'csrf':
|
||||
case "csrf":
|
||||
return res.json({ csrfToken: req.options.csrfToken })
|
||||
case 'signin':
|
||||
case "signin":
|
||||
if (pages.signIn) {
|
||||
let signinUrl = `${pages.signIn}${pages.signIn.includes('?') ? '&' : '?'}callbackUrl=${req.options.callbackUrl}`
|
||||
if (error) { signinUrl = `${signinUrl}&error=${error}` }
|
||||
let signinUrl = `${pages.signIn}${
|
||||
pages.signIn.includes("?") ? "&" : "?"
|
||||
}callbackUrl=${req.options.callbackUrl}`
|
||||
if (error) {
|
||||
signinUrl = `${signinUrl}&error=${error}`
|
||||
}
|
||||
return res.redirect(signinUrl)
|
||||
}
|
||||
|
||||
return render.signin()
|
||||
case 'signout':
|
||||
if (pages.signOut) {
|
||||
return res.redirect(`${pages.signOut}${pages.signOut.includes('?') ? '&' : '?'}error=${error}`)
|
||||
}
|
||||
case "signout":
|
||||
if (pages.signOut) return res.redirect(pages.signOut)
|
||||
|
||||
return render.signout()
|
||||
case 'callback':
|
||||
case "callback":
|
||||
if (provider) {
|
||||
if (await pkce.handleCallback(req, res)) return
|
||||
if (await state.handleCallback(req, res)) return
|
||||
return routes.callback(req, res)
|
||||
}
|
||||
break
|
||||
case 'verify-request':
|
||||
case "verify-request":
|
||||
if (pages.verifyRequest) {
|
||||
return res.redirect(pages.verifyRequest)
|
||||
}
|
||||
return render.verifyRequest()
|
||||
case 'error':
|
||||
case "error":
|
||||
if (pages.error) {
|
||||
return res.redirect(`${pages.error}${pages.error.includes('?') ? '&' : '?'}error=${error}`)
|
||||
return res.redirect(
|
||||
`${pages.error}${
|
||||
pages.error.includes("?") ? "&" : "?"
|
||||
}error=${error}`
|
||||
)
|
||||
}
|
||||
|
||||
// These error messages are displayed in line on the sign in page
|
||||
if ([
|
||||
'Signin',
|
||||
'OAuthSignin',
|
||||
'OAuthCallback',
|
||||
'OAuthCreateAccount',
|
||||
'EmailCreateAccount',
|
||||
'Callback',
|
||||
'OAuthAccountNotLinked',
|
||||
'EmailSignin',
|
||||
'CredentialsSignin'
|
||||
].includes(error)) {
|
||||
if (
|
||||
[
|
||||
"Signin",
|
||||
"OAuthSignin",
|
||||
"OAuthCallback",
|
||||
"OAuthCreateAccount",
|
||||
"EmailCreateAccount",
|
||||
"Callback",
|
||||
"OAuthAccountNotLinked",
|
||||
"EmailSignin",
|
||||
"CredentialsSignin",
|
||||
].includes(error)
|
||||
) {
|
||||
return res.redirect(`${baseUrl}${basePath}/signin?error=${error}`)
|
||||
}
|
||||
|
||||
return render.error({ error })
|
||||
default:
|
||||
}
|
||||
} else if (req.method === 'POST') {
|
||||
} else if (req.method === "POST") {
|
||||
switch (action) {
|
||||
case 'signin':
|
||||
case "signin":
|
||||
// Verified CSRF Token required for all sign in routes
|
||||
if (req.options.csrfTokenVerified && provider) {
|
||||
if (await pkce.handleSignin(req, res)) return
|
||||
@@ -208,16 +238,19 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
}
|
||||
|
||||
return res.redirect(`${baseUrl}${basePath}/signin?csrf=true`)
|
||||
case 'signout':
|
||||
case "signout":
|
||||
// Verified CSRF Token required for signout
|
||||
if (req.options.csrfTokenVerified) {
|
||||
return routes.signout(req, res)
|
||||
}
|
||||
return res.redirect(`${baseUrl}${basePath}/signout?csrf=true`)
|
||||
case 'callback':
|
||||
case "callback":
|
||||
if (provider) {
|
||||
// Verified CSRF Token required for credentials providers only
|
||||
if (provider.type === 'credentials' && !req.options.csrfTokenVerified) {
|
||||
if (
|
||||
provider.type === "credentials" &&
|
||||
!req.options.csrfTokenVerified
|
||||
) {
|
||||
return res.redirect(`${baseUrl}${basePath}/signin?csrf=true`)
|
||||
}
|
||||
|
||||
@@ -226,31 +259,33 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
return routes.callback(req, res)
|
||||
}
|
||||
break
|
||||
case '_log':
|
||||
case "_log":
|
||||
if (userOptions.logger) {
|
||||
try {
|
||||
const {
|
||||
code = 'CLIENT_ERROR',
|
||||
level = 'error',
|
||||
message = '[]'
|
||||
code = "CLIENT_ERROR",
|
||||
level = "error",
|
||||
message = "[]",
|
||||
} = req.body
|
||||
|
||||
logger[level](code, ...JSON.parse(message))
|
||||
} catch (error) {
|
||||
// If logging itself failed...
|
||||
logger.error('LOGGER_ERROR', error)
|
||||
logger.error("LOGGER_ERROR", error)
|
||||
}
|
||||
}
|
||||
return res.end()
|
||||
default:
|
||||
}
|
||||
}
|
||||
return res.status(400).end(`Error: HTTP ${req.method} is not supported for ${req.url}`)
|
||||
return res
|
||||
.status(400)
|
||||
.end(`Error: HTTP ${req.method} is not supported for ${req.url}`)
|
||||
})
|
||||
}
|
||||
|
||||
/** Tha main entry point to next-auth */
|
||||
export default function NextAuth (...args) {
|
||||
export default function NextAuth(...args) {
|
||||
if (args.length === 1) {
|
||||
return (req, res) => NextAuthHandler(req, res, args[0])
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AccountNotLinkedError } from '../../lib/errors'
|
||||
import dispatchEvent from '../lib/dispatch-event'
|
||||
import { AccountNotLinkedError } from "../../lib/errors"
|
||||
import dispatchEvent from "../lib/dispatch-event"
|
||||
import adapterErrorHandler from "../../adapters/error-handler"
|
||||
|
||||
/**
|
||||
* This function handles the complex flow of signing users in, and either creating,
|
||||
@@ -12,20 +13,29 @@ import dispatchEvent from '../lib/dispatch-event'
|
||||
* All verification (e.g. OAuth flows or email address verificaiton flows) are
|
||||
* done prior to this handler being called to avoid additonal complexity in this
|
||||
* handler.
|
||||
* @param {import("types").Session} sessionToken
|
||||
* @param {import("types").Profile} profile
|
||||
* @param {import("types").Account} account
|
||||
* @param {import("types/internals").AppOptions} options
|
||||
*/
|
||||
export default async function callbackHandler (sessionToken, profile, providerAccount, options) {
|
||||
export default async function callbackHandler(
|
||||
sessionToken,
|
||||
profile,
|
||||
providerAccount,
|
||||
options
|
||||
) {
|
||||
// Input validation
|
||||
if (!profile) throw new Error('Missing profile')
|
||||
if (!providerAccount?.id || !providerAccount.type) throw new Error('Missing or invalid provider account')
|
||||
if (!['email', 'oauth'].includes(providerAccount.type)) throw new Error('Provider not supported')
|
||||
if (!profile) throw new Error("Missing profile")
|
||||
if (!providerAccount?.id || !providerAccount.type)
|
||||
throw new Error("Missing or invalid provider account")
|
||||
if (!["email", "oauth"].includes(providerAccount.type))
|
||||
throw new Error("Provider not supported")
|
||||
|
||||
const {
|
||||
adapter,
|
||||
jwt,
|
||||
events,
|
||||
session: {
|
||||
jwt: useJwtSession
|
||||
}
|
||||
session: { jwt: useJwtSession },
|
||||
} = options
|
||||
|
||||
// If no adapter is configured then we don't have a database and cannot
|
||||
@@ -34,7 +44,7 @@ export default async function callbackHandler (sessionToken, profile, providerAc
|
||||
return {
|
||||
user: profile,
|
||||
account: providerAccount,
|
||||
session: {}
|
||||
session: {},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,8 +57,8 @@ export default async function callbackHandler (sessionToken, profile, providerAc
|
||||
linkAccount,
|
||||
createSession,
|
||||
getSession,
|
||||
deleteSession
|
||||
} = await adapter.getAdapter(options)
|
||||
deleteSession,
|
||||
} = adapterErrorHandler(await adapter.getAdapter(options), options.logger)
|
||||
|
||||
let session = null
|
||||
let user = null
|
||||
@@ -74,9 +84,11 @@ export default async function callbackHandler (sessionToken, profile, providerAc
|
||||
}
|
||||
}
|
||||
|
||||
if (providerAccount.type === 'email') {
|
||||
if (providerAccount.type === "email") {
|
||||
// If signing in with an email, check if an account with the same email address exists already
|
||||
const userByEmail = profile.email ? await getUserByEmail(profile.email) : null
|
||||
const userByEmail = profile.email
|
||||
? await getUserByEmail(profile.email)
|
||||
: null
|
||||
if (userByEmail) {
|
||||
// If they are not already signed in as the same user, this flow will
|
||||
// sign them out of the current session and sign them in as the new user
|
||||
@@ -107,11 +119,14 @@ export default async function callbackHandler (sessionToken, profile, providerAc
|
||||
return {
|
||||
session,
|
||||
user,
|
||||
isNewUser
|
||||
isNewUser,
|
||||
}
|
||||
} else if (providerAccount.type === 'oauth') {
|
||||
} else if (providerAccount.type === "oauth") {
|
||||
// If signing in with oauth account, check to see if the account exists already
|
||||
const userByProviderAccountId = await getUserByProviderAccountId(providerAccount.provider, providerAccount.id)
|
||||
const userByProviderAccountId = await getUserByProviderAccountId(
|
||||
providerAccount.provider,
|
||||
providerAccount.id
|
||||
)
|
||||
if (userByProviderAccountId) {
|
||||
if (isSignedIn) {
|
||||
// If the user is already signed in with this account, we don't need to do anything
|
||||
@@ -122,7 +137,7 @@ export default async function callbackHandler (sessionToken, profile, providerAc
|
||||
return {
|
||||
session,
|
||||
user,
|
||||
isNewUser
|
||||
isNewUser,
|
||||
}
|
||||
}
|
||||
// If the user is currently signed in, but the new account they are signing in
|
||||
@@ -132,11 +147,13 @@ export default async function callbackHandler (sessionToken, profile, providerAc
|
||||
}
|
||||
// If there is no active session, but the account being signed in with is already
|
||||
// associated with a valid user then create session to sign the user in.
|
||||
session = useJwtSession ? {} : await createSession(userByProviderAccountId)
|
||||
session = useJwtSession
|
||||
? {}
|
||||
: await createSession(userByProviderAccountId)
|
||||
return {
|
||||
session,
|
||||
user: userByProviderAccountId,
|
||||
isNewUser
|
||||
isNewUser,
|
||||
}
|
||||
} else {
|
||||
if (isSignedIn) {
|
||||
@@ -151,13 +168,16 @@ export default async function callbackHandler (sessionToken, profile, providerAc
|
||||
providerAccount.accessToken,
|
||||
providerAccount.accessTokenExpires
|
||||
)
|
||||
await dispatchEvent(events.linkAccount, { user, providerAccount: providerAccount })
|
||||
await dispatchEvent(events.linkAccount, {
|
||||
user,
|
||||
providerAccount: providerAccount,
|
||||
})
|
||||
|
||||
// As they are already signed in, we don't need to do anything after linking them
|
||||
return {
|
||||
session,
|
||||
user,
|
||||
isNewUser
|
||||
isNewUser,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +198,9 @@ export default async function callbackHandler (sessionToken, profile, providerAc
|
||||
//
|
||||
// OAuth providers should require email address verification to prevent this, but in
|
||||
// practice that is not always the case; this helps protect against that.
|
||||
const userByEmail = profile.email ? await getUserByEmail(profile.email) : null
|
||||
const userByEmail = profile.email
|
||||
? await getUserByEmail(profile.email)
|
||||
: null
|
||||
if (userByEmail) {
|
||||
// We end up here when we don't have an account with the same [provider].id *BUT*
|
||||
// we do already have an account with the same email address as the one in the
|
||||
@@ -207,14 +229,17 @@ export default async function callbackHandler (sessionToken, profile, providerAc
|
||||
providerAccount.accessToken,
|
||||
providerAccount.accessTokenExpires
|
||||
)
|
||||
await dispatchEvent(events.linkAccount, { user, providerAccount: providerAccount })
|
||||
await dispatchEvent(events.linkAccount, {
|
||||
user,
|
||||
providerAccount: providerAccount,
|
||||
})
|
||||
|
||||
session = useJwtSession ? {} : await createSession(user)
|
||||
isNewUser = true
|
||||
return {
|
||||
session,
|
||||
user,
|
||||
isNewUser
|
||||
isNewUser,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,6 +215,7 @@ async function getOAuth2AccessToken (code, provider, codeVerifier) {
|
||||
*/
|
||||
async function getOAuth2 (provider, accessToken, results) {
|
||||
let url = provider.profileUrl
|
||||
let httpMethod = 'GET'
|
||||
const headers = { ...provider.headers }
|
||||
|
||||
if (this._useAuthorizationHeaderForGET) {
|
||||
@@ -238,8 +239,15 @@ async function getOAuth2 (provider, accessToken, results) {
|
||||
url = prepareProfileUrl({ provider, url, results })
|
||||
}
|
||||
|
||||
/** Dropbox requires POST instead of GET
|
||||
* Read more: https://www.dropbox.com/developers/reference/auth-types#user
|
||||
*/
|
||||
if (provider.id === 'dropbox') {
|
||||
httpMethod = 'POST'
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this._request('GET', url, headers, null, accessToken, (error, profileData) => {
|
||||
this._request(httpMethod, url, headers, null, accessToken, (error, profileData) => {
|
||||
if (error) {
|
||||
return reject(error)
|
||||
}
|
||||
|
||||
@@ -1,22 +1,44 @@
|
||||
import { randomBytes } from 'crypto'
|
||||
import { randomBytes } from "crypto"
|
||||
import adapterErrorHandler from "../../../adapters/error-handler"
|
||||
|
||||
export default async function email (email, provider, options) {
|
||||
/**
|
||||
*
|
||||
* @param {string} email
|
||||
* @param {import("types/providers").EmailConfig} provider
|
||||
* @param {import("types/internals").AppOptions} options
|
||||
* @returns
|
||||
*/
|
||||
export default async function email(email, provider, options) {
|
||||
try {
|
||||
const { baseUrl, basePath, adapter } = options
|
||||
const { baseUrl, basePath, adapter, logger } = options
|
||||
|
||||
const { createVerificationRequest } = await adapter.getAdapter(options)
|
||||
const { createVerificationRequest } = adapterErrorHandler(
|
||||
await adapter.getAdapter(options),
|
||||
logger
|
||||
)
|
||||
|
||||
// Prefer provider specific secret, but use default secret if none specified
|
||||
const secret = provider.secret || options.secret
|
||||
|
||||
// Generate token
|
||||
const token = await provider.generateVerificationToken?.() ?? randomBytes(32).toString('hex')
|
||||
const token =
|
||||
(await provider.generateVerificationToken?.()) ??
|
||||
randomBytes(32).toString("hex")
|
||||
|
||||
// Send email with link containing token (the unhashed version)
|
||||
const url = `${baseUrl}${basePath}/callback/${encodeURIComponent(provider.id)}?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}`
|
||||
const url = `${baseUrl}${basePath}/callback/${encodeURIComponent(
|
||||
provider.id
|
||||
)}?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}`
|
||||
|
||||
// @TODO Create invite (send secret so can be hashed)
|
||||
await createVerificationRequest(email, url, token, secret, provider, options)
|
||||
await createVerificationRequest(
|
||||
email,
|
||||
url,
|
||||
token,
|
||||
secret,
|
||||
provider,
|
||||
options
|
||||
)
|
||||
|
||||
// Return promise
|
||||
return Promise.resolve()
|
||||
|
||||
@@ -15,9 +15,9 @@ export default async function getAuthorizationUrl (req) {
|
||||
if (provider.version?.startsWith('2.')) {
|
||||
// Handle OAuth v2.x
|
||||
let url = client.getAuthorizeUrl({
|
||||
scope: provider.scope,
|
||||
...params,
|
||||
redirect_uri: provider.callbackUrl,
|
||||
scope: provider.scope
|
||||
redirect_uri: provider.callbackUrl
|
||||
})
|
||||
|
||||
// If the authorizationUrl specified in the config has query parameters on it
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { h } from 'preact' // eslint-disable-line no-unused-vars
|
||||
import { h } from "preact" // eslint-disable-line no-unused-vars
|
||||
|
||||
export default function signin ({ csrfToken, providers, callbackUrl, email, error: errorType }) {
|
||||
export default function signin({
|
||||
csrfToken,
|
||||
providers,
|
||||
callbackUrl,
|
||||
email,
|
||||
error: errorType,
|
||||
}) {
|
||||
// We only want to render providers
|
||||
const providersToRender = providers.filter(provider => {
|
||||
if (provider.type === 'oauth' || provider.type === 'email') {
|
||||
const providersToRender = providers.filter((provider) => {
|
||||
if (provider.type === "oauth" || provider.type === "email") {
|
||||
// Always render oauth and email type providers
|
||||
return true
|
||||
} else if (provider.type === 'credentials' && provider.credentials) {
|
||||
} else if (provider.type === "credentials" && provider.credentials) {
|
||||
// Only render credentials type provider if credentials are defined
|
||||
return true
|
||||
}
|
||||
@@ -15,70 +21,93 @@ export default function signin ({ csrfToken, providers, callbackUrl, email, erro
|
||||
})
|
||||
|
||||
const errors = {
|
||||
Signin: 'Try signing with a different account.',
|
||||
OAuthSignin: 'Try signing with a different account.',
|
||||
OAuthCallback: 'Try signing with a different account.',
|
||||
OAuthCreateAccount: 'Try signing with a different account.',
|
||||
EmailCreateAccount: 'Try signing with a different account.',
|
||||
Callback: 'Try signing with a different account.',
|
||||
OAuthAccountNotLinked: 'To confirm your identity, sign in with the same account you used originally.',
|
||||
EmailSignin: 'Check your email address.',
|
||||
CredentialsSignin: 'Sign in failed. Check the details you provided are correct.',
|
||||
default: 'Unable to sign in.'
|
||||
Signin: "Try signing in with a different account.",
|
||||
OAuthSignin: "Try signing in with a different account.",
|
||||
OAuthCallback: "Try signing in with a different account.",
|
||||
OAuthCreateAccount: "Try signing in with a different account.",
|
||||
EmailCreateAccount: "Try signing in with a different account.",
|
||||
Callback: "Try signing in with a different account.",
|
||||
OAuthAccountNotLinked:
|
||||
"To confirm your identity, sign in with the same account you used originally.",
|
||||
EmailSignin: "Check your email inbox.",
|
||||
CredentialsSignin:
|
||||
"Sign in failed. Check the details you provided are correct.",
|
||||
default: "Unable to sign in.",
|
||||
}
|
||||
|
||||
const error = errorType && (errors[errorType] ?? errors.default)
|
||||
|
||||
return (
|
||||
<div className='signin'>
|
||||
{error &&
|
||||
<div className='error'>
|
||||
<div className="signin">
|
||||
{error && (
|
||||
<div className="error">
|
||||
<p>{error}</p>
|
||||
</div>}
|
||||
{providersToRender.map((provider, i) =>
|
||||
<div key={provider.id} className='provider'>
|
||||
{provider.type === 'oauth' &&
|
||||
<form action={provider.signinUrl} method='POST'>
|
||||
<input type='hidden' name='csrfToken' value={csrfToken} />
|
||||
{callbackUrl && <input type='hidden' name='callbackUrl' value={callbackUrl} />}
|
||||
<button type='submit' className='button'>Sign in with {provider.name}</button>
|
||||
</form>}
|
||||
{(provider.type === 'email' || provider.type === 'credentials') && (i > 0) &&
|
||||
providersToRender[i - 1].type !== 'email' && providersToRender[i - 1].type !== 'credentials' &&
|
||||
<hr />}
|
||||
{provider.type === 'email' &&
|
||||
<form action={provider.signinUrl} method='POST'>
|
||||
<input type='hidden' name='csrfToken' value={csrfToken} />
|
||||
<label for={`input-email-for-${provider.id}-provider`}>Email</label>
|
||||
<input id={`input-email-for-${provider.id}-provider`} autoFocus type='text' name='email' value={email} placeholder='email@example.com' />
|
||||
<button type='submit'>Sign in with {provider.name}</button>
|
||||
</form>}
|
||||
{provider.type === 'credentials' &&
|
||||
<form action={provider.callbackUrl} method='POST'>
|
||||
<input type='hidden' name='csrfToken' value={csrfToken} />
|
||||
{Object.keys(provider.credentials).map(credential => {
|
||||
</div>
|
||||
)}
|
||||
{providersToRender.map((provider, i) => (
|
||||
<div key={provider.id} className="provider">
|
||||
{provider.type === "oauth" && (
|
||||
<form action={provider.signinUrl} method="POST">
|
||||
<input type="hidden" name="csrfToken" value={csrfToken} />
|
||||
{callbackUrl && (
|
||||
<input type="hidden" name="callbackUrl" value={callbackUrl} />
|
||||
)}
|
||||
<button type="submit" className="button">
|
||||
Sign in with {provider.name}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
{(provider.type === "email" || provider.type === "credentials") &&
|
||||
i > 0 &&
|
||||
providersToRender[i - 1].type !== "email" &&
|
||||
providersToRender[i - 1].type !== "credentials" && <hr />}
|
||||
{provider.type === "email" && (
|
||||
<form action={provider.signinUrl} method="POST">
|
||||
<input type="hidden" name="csrfToken" value={csrfToken} />
|
||||
<label for={`input-email-for-${provider.id}-provider`}>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
id={`input-email-for-${provider.id}-provider`}
|
||||
autoFocus
|
||||
type="text"
|
||||
name="email"
|
||||
value={email}
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
<button type="submit">Sign in with {provider.name}</button>
|
||||
</form>
|
||||
)}
|
||||
{provider.type === "credentials" && (
|
||||
<form action={provider.callbackUrl} method="POST">
|
||||
<input type="hidden" name="csrfToken" value={csrfToken} />
|
||||
{Object.keys(provider.credentials).map((credential) => {
|
||||
return (
|
||||
<div key={`input-group-${provider.id}`}>
|
||||
<label
|
||||
for={`input-${credential}-for-${provider.id}-provider`}
|
||||
>{provider.credentials[credential].label || credential}
|
||||
>
|
||||
{provider.credentials[credential].label || credential}
|
||||
</label>
|
||||
<input
|
||||
name={credential}
|
||||
id={`input-${credential}-for-${provider.id}-provider`}
|
||||
type={provider.credentials[credential].type || 'text'}
|
||||
value={provider.credentials[credential].value || ''}
|
||||
placeholder={provider.credentials[credential].placeholder || ''}
|
||||
type={provider.credentials[credential].type || "text"}
|
||||
value={provider.credentials[credential].value || ""}
|
||||
placeholder={
|
||||
provider.credentials[credential].placeholder || ""
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<button type='submit'>Sign in with {provider.name}</button>
|
||||
</form>}
|
||||
{(provider.type === 'email' || provider.type === 'credentials') && ((i + 1) < providersToRender.length) &&
|
||||
<hr />}
|
||||
<button type="submit">Sign in with {provider.name}</button>
|
||||
</form>
|
||||
)}
|
||||
{(provider.type === "email" || provider.type === "credentials") &&
|
||||
i + 1 < providersToRender.length && <hr />}
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import oAuthCallback from '../lib/oauth/callback'
|
||||
import callbackHandler from '../lib/callback-handler'
|
||||
import * as cookie from '../lib/cookie'
|
||||
import logger from '../../lib/logger'
|
||||
import dispatchEvent from '../lib/dispatch-event'
|
||||
import oAuthCallback from "../lib/oauth/callback"
|
||||
import callbackHandler from "../lib/callback-handler"
|
||||
import * as cookie from "../lib/cookie"
|
||||
import dispatchEvent from "../lib/dispatch-event"
|
||||
import adapterErrorHandler from "../../adapters/error-handler"
|
||||
|
||||
/**
|
||||
* Handle callbacks from login services
|
||||
* @param {import("types/internals").NextAuthRequest} req
|
||||
* @param {import("types/internals").NextAuthResponse} res
|
||||
*/
|
||||
export default async function callback (req, res) {
|
||||
export default async function callback(req, res) {
|
||||
const {
|
||||
provider,
|
||||
adapter,
|
||||
@@ -22,21 +22,23 @@ export default async function callback (req, res) {
|
||||
jwt,
|
||||
events,
|
||||
callbacks,
|
||||
session: {
|
||||
jwt: useJwtSession,
|
||||
maxAge: sessionMaxAge
|
||||
}
|
||||
session: { jwt: useJwtSession, maxAge: sessionMaxAge },
|
||||
logger,
|
||||
} = req.options
|
||||
|
||||
// Get session ID (if set)
|
||||
const sessionToken = req.cookies?.[cookies.sessionToken.name] ?? null
|
||||
|
||||
if (provider.type === 'oauth') {
|
||||
if (provider.type === "oauth") {
|
||||
try {
|
||||
const { profile, account, OAuthProfile } = await oAuthCallback(req)
|
||||
try {
|
||||
// Make it easier to debug when adding a new provider
|
||||
logger.debug('OAUTH_CALLBACK_RESPONSE', { profile, account, OAuthProfile })
|
||||
logger.debug("OAUTH_CALLBACK_RESPONSE", {
|
||||
profile,
|
||||
account,
|
||||
OAuthProfile,
|
||||
})
|
||||
|
||||
// If we don't have a profile object then either something went wrong
|
||||
// or the user cancelled signing in. We don't know which, so we just
|
||||
@@ -56,52 +58,85 @@ export default async function callback (req, res) {
|
||||
// (that just means it's a new user signing in for the first time).
|
||||
let userOrProfile = profile
|
||||
if (adapter) {
|
||||
const { getUserByProviderAccountId } = await adapter.getAdapter(req.options)
|
||||
const userFromProviderAccountId = await getUserByProviderAccountId(account.provider, account.id)
|
||||
const { getUserByProviderAccountId } = adapterErrorHandler(
|
||||
await adapter.getAdapter(req.options),
|
||||
logger
|
||||
)
|
||||
const userFromProviderAccountId = await getUserByProviderAccountId(
|
||||
account.provider,
|
||||
account.id
|
||||
)
|
||||
if (userFromProviderAccountId) {
|
||||
userOrProfile = userFromProviderAccountId
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const signInCallbackResponse = await callbacks.signIn(userOrProfile, account, OAuthProfile)
|
||||
const signInCallbackResponse = await callbacks.signIn(
|
||||
userOrProfile,
|
||||
account,
|
||||
OAuthProfile
|
||||
)
|
||||
if (signInCallbackResponse === false) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
|
||||
} else if (typeof signInCallbackResponse === 'string') {
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=AccessDenied`
|
||||
)
|
||||
} else if (typeof signInCallbackResponse === "string") {
|
||||
return res.redirect(signInCallbackResponse)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
|
||||
error.message
|
||||
)}`
|
||||
)
|
||||
}
|
||||
// TODO: Remove in a future major release
|
||||
logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT')
|
||||
logger.warn("SIGNIN_CALLBACK_REJECT_REDIRECT")
|
||||
return res.redirect(error)
|
||||
}
|
||||
|
||||
// Sign user in
|
||||
const { user, session, isNewUser } = await callbackHandler(sessionToken, profile, account, req.options)
|
||||
const { user, session, isNewUser } = await callbackHandler(
|
||||
sessionToken,
|
||||
profile,
|
||||
account,
|
||||
req.options
|
||||
)
|
||||
|
||||
if (useJwtSession) {
|
||||
const defaultJwtPayload = {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
picture: user.image,
|
||||
sub: user.id?.toString()
|
||||
sub: user.id?.toString(),
|
||||
}
|
||||
const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, OAuthProfile, isNewUser)
|
||||
const jwtPayload = await callbacks.jwt(
|
||||
defaultJwtPayload,
|
||||
user,
|
||||
account,
|
||||
OAuthProfile,
|
||||
isNewUser
|
||||
)
|
||||
|
||||
// Sign and encrypt token
|
||||
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
|
||||
|
||||
// Set cookie expiry date
|
||||
const cookieExpires = new Date()
|
||||
cookieExpires.setTime(cookieExpires.getTime() + (sessionMaxAge * 1000))
|
||||
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)
|
||||
|
||||
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options })
|
||||
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, {
|
||||
expires: cookieExpires.toISOString(),
|
||||
...cookies.sessionToken.options,
|
||||
})
|
||||
} else {
|
||||
// Save Session Token in cookie
|
||||
cookie.set(res, cookies.sessionToken.name, session.sessionToken, { expires: session.expires || null, ...cookies.sessionToken.options })
|
||||
cookie.set(res, cookies.sessionToken.name, session.sessionToken, {
|
||||
expires: session.expires || null,
|
||||
...cookies.sessionToken.options,
|
||||
})
|
||||
}
|
||||
|
||||
await dispatchEvent(events.signIn, { user, account, isNewUser })
|
||||
@@ -110,94 +145,145 @@ export default async function callback (req, res) {
|
||||
// e.g. option to send users to a new account landing page on initial login
|
||||
// Note that the callback URL is preserved, so the journey can still be resumed
|
||||
if (isNewUser && pages.newUser) {
|
||||
return res.redirect(`${pages.newUser}${pages.newUser.includes('?') ? '&' : '?'}callbackUrl=${encodeURIComponent(callbackUrl)}`)
|
||||
return res.redirect(
|
||||
`${pages.newUser}${
|
||||
pages.newUser.includes("?") ? "&" : "?"
|
||||
}callbackUrl=${encodeURIComponent(callbackUrl)}`
|
||||
)
|
||||
}
|
||||
|
||||
// Callback URL is already verified at this point, so safe to use if specified
|
||||
return res.redirect(callbackUrl || baseUrl)
|
||||
} catch (error) {
|
||||
if (error.name === 'AccountNotLinkedError') {
|
||||
if (error.name === "AccountNotLinkedError") {
|
||||
// If the email on the account is already linked, but not with this OAuth account
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthAccountNotLinked`)
|
||||
} else if (error.name === 'CreateUserError') {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthCreateAccount`)
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=OAuthAccountNotLinked`
|
||||
)
|
||||
} else if (error.name === "CreateUserError") {
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=OAuthCreateAccount`
|
||||
)
|
||||
}
|
||||
logger.error('OAUTH_CALLBACK_HANDLER_ERROR', error)
|
||||
logger.error("OAUTH_CALLBACK_HANDLER_ERROR", error)
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=Callback`)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.name === 'OAuthCallbackError') {
|
||||
logger.error('CALLBACK_OAUTH_ERROR', error)
|
||||
if (error.name === "OAuthCallbackError") {
|
||||
logger.error("CALLBACK_OAUTH_ERROR", error)
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthCallback`)
|
||||
}
|
||||
logger.error('OAUTH_CALLBACK_ERROR', error)
|
||||
logger.error("OAUTH_CALLBACK_ERROR", error)
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=Callback`)
|
||||
}
|
||||
} else if (provider.type === 'email') {
|
||||
} else if (provider.type === "email") {
|
||||
try {
|
||||
if (!adapter) {
|
||||
logger.error('EMAIL_REQUIRES_ADAPTER_ERROR')
|
||||
logger.error("EMAIL_REQUIRES_ADAPTER_ERROR")
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
||||
}
|
||||
|
||||
const { getVerificationRequest, deleteVerificationRequest, getUserByEmail } = await adapter.getAdapter(req.options)
|
||||
const {
|
||||
getVerificationRequest,
|
||||
deleteVerificationRequest,
|
||||
getUserByEmail,
|
||||
} = adapterErrorHandler(await adapter.getAdapter(req.options), logger)
|
||||
const verificationToken = req.query.token
|
||||
const email = req.query.email
|
||||
|
||||
// Verify email and verification token exist in database
|
||||
const invite = await getVerificationRequest(email, verificationToken, secret, provider)
|
||||
const invite = await getVerificationRequest(
|
||||
email,
|
||||
verificationToken,
|
||||
secret,
|
||||
provider
|
||||
)
|
||||
if (!invite) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=Verification`)
|
||||
}
|
||||
|
||||
// If verification token is valid, delete verification request token from
|
||||
// the database so it cannot be used again
|
||||
await deleteVerificationRequest(email, verificationToken, secret, provider)
|
||||
await deleteVerificationRequest(
|
||||
email,
|
||||
verificationToken,
|
||||
secret,
|
||||
provider
|
||||
)
|
||||
|
||||
// If is an existing user return a user object (otherwise use placeholder)
|
||||
const profile = await getUserByEmail(email) || { email }
|
||||
const account = { id: provider.id, type: 'email', providerAccountId: email }
|
||||
const profile = (await getUserByEmail(email)) || { email }
|
||||
const account = {
|
||||
id: provider.id,
|
||||
type: "email",
|
||||
providerAccountId: email,
|
||||
}
|
||||
|
||||
// Check if user is allowed to sign in
|
||||
try {
|
||||
const signInCallbackResponse = await callbacks.signIn(profile, account, { email })
|
||||
const signInCallbackResponse = await callbacks.signIn(
|
||||
profile,
|
||||
account,
|
||||
{ email }
|
||||
)
|
||||
if (signInCallbackResponse === false) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
|
||||
} else if (typeof signInCallbackResponse === 'string') {
|
||||
} else if (typeof signInCallbackResponse === "string") {
|
||||
return res.redirect(signInCallbackResponse)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
|
||||
error.message
|
||||
)}`
|
||||
)
|
||||
}
|
||||
// TODO: Remove in a future major release
|
||||
logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT')
|
||||
logger.warn("SIGNIN_CALLBACK_REJECT_REDIRECT")
|
||||
return res.redirect(error)
|
||||
}
|
||||
|
||||
// Sign user in
|
||||
const { user, session, isNewUser } = await callbackHandler(sessionToken, profile, account, req.options)
|
||||
const { user, session, isNewUser } = await callbackHandler(
|
||||
sessionToken,
|
||||
profile,
|
||||
account,
|
||||
req.options
|
||||
)
|
||||
|
||||
if (useJwtSession) {
|
||||
const defaultJwtPayload = {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
picture: user.image,
|
||||
sub: user.id?.toString()
|
||||
sub: user.id?.toString(),
|
||||
}
|
||||
const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, profile, isNewUser)
|
||||
const jwtPayload = await callbacks.jwt(
|
||||
defaultJwtPayload,
|
||||
user,
|
||||
account,
|
||||
profile,
|
||||
isNewUser
|
||||
)
|
||||
|
||||
// Sign and encrypt token
|
||||
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
|
||||
|
||||
// Set cookie expiry date
|
||||
const cookieExpires = new Date()
|
||||
cookieExpires.setTime(cookieExpires.getTime() + (sessionMaxAge * 1000))
|
||||
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)
|
||||
|
||||
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options })
|
||||
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, {
|
||||
expires: cookieExpires.toISOString(),
|
||||
...cookies.sessionToken.options,
|
||||
})
|
||||
} else {
|
||||
// Save Session Token in cookie
|
||||
cookie.set(res, cookies.sessionToken.name, session.sessionToken, { expires: session.expires || null, ...cookies.sessionToken.options })
|
||||
cookie.set(res, cookies.sessionToken.name, session.sessionToken, {
|
||||
expires: session.expires || null,
|
||||
...cookies.sessionToken.options,
|
||||
})
|
||||
}
|
||||
|
||||
await dispatchEvent(events.signIn, { user, account, isNewUser })
|
||||
@@ -206,55 +292,93 @@ export default async function callback (req, res) {
|
||||
// e.g. option to send users to a new account landing page on initial login
|
||||
// Note that the callback URL is preserved, so the journey can still be resumed
|
||||
if (isNewUser && pages.newUser) {
|
||||
return res.redirect(`${pages.newUser}${pages.newUser.includes('?') ? '&' : '?'}callbackUrl=${encodeURIComponent(callbackUrl)}`)
|
||||
return res.redirect(
|
||||
`${pages.newUser}${
|
||||
pages.newUser.includes("?") ? "&" : "?"
|
||||
}callbackUrl=${encodeURIComponent(callbackUrl)}`
|
||||
)
|
||||
}
|
||||
|
||||
// Callback URL is already verified at this point, so safe to use if specified
|
||||
return res.redirect(callbackUrl || baseUrl)
|
||||
} catch (error) {
|
||||
if (error.name === 'CreateUserError') {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=EmailCreateAccount`)
|
||||
if (error.name === "CreateUserError") {
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=EmailCreateAccount`
|
||||
)
|
||||
}
|
||||
logger.error('CALLBACK_EMAIL_ERROR', error)
|
||||
logger.error("CALLBACK_EMAIL_ERROR", error)
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=Callback`)
|
||||
}
|
||||
} else if (provider.type === 'credentials' && req.method === 'POST') {
|
||||
} else if (provider.type === "credentials" && req.method === "POST") {
|
||||
if (!useJwtSession) {
|
||||
logger.error('CALLBACK_CREDENTIALS_JWT_ERROR', 'Signin in with credentials is only supported if JSON Web Tokens are enabled')
|
||||
return res.status(500).redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
||||
logger.error(
|
||||
"CALLBACK_CREDENTIALS_JWT_ERROR",
|
||||
"Signin in with credentials is only supported if JSON Web Tokens are enabled"
|
||||
)
|
||||
return res
|
||||
.status(500)
|
||||
.redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
||||
}
|
||||
|
||||
if (!provider.authorize) {
|
||||
logger.error('CALLBACK_CREDENTIALS_HANDLER_ERROR', 'Must define an authorize() handler to use credentials authentication provider')
|
||||
return res.status(500).redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
||||
logger.error(
|
||||
"CALLBACK_CREDENTIALS_HANDLER_ERROR",
|
||||
"Must define an authorize() handler to use credentials authentication provider"
|
||||
)
|
||||
return res
|
||||
.status(500)
|
||||
.redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
||||
}
|
||||
|
||||
const credentials = req.body
|
||||
|
||||
let userObjectReturnedFromAuthorizeHandler
|
||||
try {
|
||||
userObjectReturnedFromAuthorizeHandler = await provider.authorize(credentials)
|
||||
userObjectReturnedFromAuthorizeHandler = await provider.authorize(
|
||||
credentials, {...req, options: {}, cookies: {}}
|
||||
)
|
||||
if (!userObjectReturnedFromAuthorizeHandler) {
|
||||
return res.status(401).redirect(`${baseUrl}${basePath}/error?error=CredentialsSignin&provider=${encodeURIComponent(provider.id)}`)
|
||||
return res
|
||||
.status(401)
|
||||
.redirect(
|
||||
`${baseUrl}${basePath}/error?error=CredentialsSignin&provider=${encodeURIComponent(
|
||||
provider.id
|
||||
)}`
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
|
||||
error.message
|
||||
)}`
|
||||
)
|
||||
}
|
||||
return res.redirect(error)
|
||||
}
|
||||
|
||||
const user = userObjectReturnedFromAuthorizeHandler
|
||||
const account = { id: provider.id, type: 'credentials' }
|
||||
const account = { id: provider.id, type: "credentials" }
|
||||
|
||||
try {
|
||||
const signInCallbackResponse = await callbacks.signIn(user, account, credentials)
|
||||
const signInCallbackResponse = await callbacks.signIn(
|
||||
user,
|
||||
account,
|
||||
credentials
|
||||
)
|
||||
if (signInCallbackResponse === false) {
|
||||
return res.status(403).redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
|
||||
return res
|
||||
.status(403)
|
||||
.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
|
||||
error.message
|
||||
)}`
|
||||
)
|
||||
}
|
||||
return res.redirect(error)
|
||||
}
|
||||
@@ -263,22 +387,33 @@ export default async function callback (req, res) {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
picture: user.image,
|
||||
sub: user.id?.toString()
|
||||
sub: user.id?.toString(),
|
||||
}
|
||||
const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, userObjectReturnedFromAuthorizeHandler, false)
|
||||
const jwtPayload = await callbacks.jwt(
|
||||
defaultJwtPayload,
|
||||
user,
|
||||
account,
|
||||
userObjectReturnedFromAuthorizeHandler,
|
||||
false
|
||||
)
|
||||
|
||||
// Sign and encrypt token
|
||||
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
|
||||
|
||||
// Set cookie expiry date
|
||||
const cookieExpires = new Date()
|
||||
cookieExpires.setTime(cookieExpires.getTime() + (sessionMaxAge * 1000))
|
||||
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)
|
||||
|
||||
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options })
|
||||
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, {
|
||||
expires: cookieExpires.toISOString(),
|
||||
...cookies.sessionToken.options,
|
||||
})
|
||||
|
||||
await dispatchEvent(events.signIn, { user, account })
|
||||
|
||||
return res.redirect(callbackUrl || baseUrl)
|
||||
}
|
||||
return res.status(500).end(`Error: Callback for provider type ${provider.type} not supported`)
|
||||
return res
|
||||
.status(500)
|
||||
.end(`Error: Callback for provider type ${provider.type} not supported`)
|
||||
}
|
||||
|
||||
@@ -5,13 +5,16 @@
|
||||
* @param {import("types/internals").NextAuthRequest} req
|
||||
* @param {import("types/internals").NextAuthResponse} res
|
||||
*/
|
||||
export default function providers (req, res) {
|
||||
export default function providers(req, res) {
|
||||
const { providers } = req.options
|
||||
|
||||
const result = providers.reduce((acc, { id, name, type, signinUrl, callbackUrl }) => {
|
||||
acc[id] = { id, name, type, signinUrl, callbackUrl }
|
||||
return acc
|
||||
}, {})
|
||||
const result = providers.reduce(
|
||||
(acc, { id, name, type, signinUrl, callbackUrl }) => {
|
||||
acc[id] = { id, name, type, signinUrl, callbackUrl }
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
res.json(result)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import * as cookie from '../lib/cookie'
|
||||
import logger from '../../lib/logger'
|
||||
import dispatchEvent from '../lib/dispatch-event'
|
||||
import * as cookie from "../lib/cookie"
|
||||
import dispatchEvent from "../lib/dispatch-event"
|
||||
import adapterErrorHandler from "../../adapters/error-handler"
|
||||
|
||||
/**
|
||||
* Return a session object (without any private fields)
|
||||
* for Single Page App clients
|
||||
* @param {import("types/internals").NextAuthRequest} req
|
||||
* @param {import("types/internals").NextAuthResponse} res
|
||||
*/
|
||||
export default async function session (req, res) {
|
||||
const { cookies, adapter, jwt, events, callbacks } = req.options
|
||||
export default async function session(req, res) {
|
||||
const { cookies, adapter, jwt, events, callbacks, logger } = req.options
|
||||
const useJwtSession = req.options.session.jwt
|
||||
const sessionMaxAge = req.options.session.maxAge
|
||||
const sessionToken = req.cookies[cookies.sessionToken.name]
|
||||
@@ -24,7 +26,9 @@ export default async function session (req, res) {
|
||||
|
||||
// Generate new session expiry date
|
||||
const sessionExpiresDate = new Date()
|
||||
sessionExpiresDate.setTime(sessionExpiresDate.getTime() + (sessionMaxAge * 1000))
|
||||
sessionExpiresDate.setTime(
|
||||
sessionExpiresDate.getTime() + sessionMaxAge * 1000
|
||||
)
|
||||
const sessionExpires = sessionExpiresDate.toISOString()
|
||||
|
||||
// By default, only exposes a limited subset of information to the client
|
||||
@@ -33,14 +37,17 @@ export default async function session (req, res) {
|
||||
user: {
|
||||
name: decodedJwt.name || null,
|
||||
email: decodedJwt.email || null,
|
||||
image: decodedJwt.picture || null
|
||||
image: decodedJwt.picture || null,
|
||||
},
|
||||
expires: sessionExpires
|
||||
expires: sessionExpires,
|
||||
}
|
||||
|
||||
// Pass Session and JSON Web Token through to the session callback
|
||||
const jwtPayload = await callbacks.jwt(decodedJwt)
|
||||
const sessionPayload = await callbacks.session(defaultSessionPayload, jwtPayload)
|
||||
const sessionPayload = await callbacks.session(
|
||||
defaultSessionPayload,
|
||||
jwtPayload
|
||||
)
|
||||
|
||||
// Return session payload as response
|
||||
response = sessionPayload
|
||||
@@ -49,17 +56,29 @@ export default async function session (req, res) {
|
||||
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
|
||||
|
||||
// Set cookie, to also update expiry date on cookie
|
||||
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: sessionExpires, ...cookies.sessionToken.options })
|
||||
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, {
|
||||
expires: sessionExpires,
|
||||
...cookies.sessionToken.options,
|
||||
})
|
||||
|
||||
await dispatchEvent(events.session, { session: sessionPayload, jwt: jwtPayload })
|
||||
await dispatchEvent(events.session, {
|
||||
session: sessionPayload,
|
||||
jwt: jwtPayload,
|
||||
})
|
||||
} catch (error) {
|
||||
// If JWT not verifiable, make sure the cookie for it is removed and return empty object
|
||||
logger.error('JWT_SESSION_ERROR', error)
|
||||
cookie.set(res, cookies.sessionToken.name, '', { ...cookies.sessionToken.options, maxAge: 0 })
|
||||
logger.error("JWT_SESSION_ERROR", error)
|
||||
cookie.set(res, cookies.sessionToken.name, "", {
|
||||
...cookies.sessionToken.options,
|
||||
maxAge: 0,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const { getUser, getSession, updateSession } = await adapter.getAdapter(req.options)
|
||||
const { getUser, getSession, updateSession } = adapterErrorHandler(
|
||||
await adapter.getAdapter(req.options),
|
||||
logger
|
||||
)
|
||||
const session = await getSession(sessionToken)
|
||||
if (session) {
|
||||
// Trigger update to session object to update session expiry
|
||||
@@ -73,29 +92,38 @@ export default async function session (req, res) {
|
||||
user: {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
image: user.image
|
||||
image: user.image,
|
||||
},
|
||||
accessToken: session.accessToken,
|
||||
expires: session.expires
|
||||
expires: session.expires,
|
||||
}
|
||||
|
||||
// Pass Session through to the session callback
|
||||
const sessionPayload = await callbacks.session(defaultSessionPayload, user)
|
||||
const sessionPayload = await callbacks.session(
|
||||
defaultSessionPayload,
|
||||
user
|
||||
)
|
||||
|
||||
// Return session payload as response
|
||||
response = sessionPayload
|
||||
|
||||
// Set cookie again to update expiry
|
||||
cookie.set(res, cookies.sessionToken.name, sessionToken, { expires: session.expires, ...cookies.sessionToken.options })
|
||||
cookie.set(res, cookies.sessionToken.name, sessionToken, {
|
||||
expires: session.expires,
|
||||
...cookies.sessionToken.options,
|
||||
})
|
||||
|
||||
await dispatchEvent(events.session, { session: sessionPayload })
|
||||
} else if (sessionToken) {
|
||||
// If sessionToken was found set but it's not valid for a session then
|
||||
// remove the sessionToken cookie from browser.
|
||||
cookie.set(res, cookies.sessionToken.name, '', { ...cookies.sessionToken.options, maxAge: 0 })
|
||||
cookie.set(res, cookies.sessionToken.name, "", {
|
||||
...cookies.sessionToken.options,
|
||||
maxAge: 0,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('SESSION_ERROR', error)
|
||||
logger.error("SESSION_ERROR", error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,35 +1,43 @@
|
||||
import getAuthorizationUrl from '../lib/signin/oauth'
|
||||
import emailSignin from '../lib/signin/email'
|
||||
import logger from '../../lib/logger'
|
||||
import getAuthorizationUrl from "../lib/signin/oauth"
|
||||
import emailSignin from "../lib/signin/email"
|
||||
import adapterErrorHandler from "../../adapters/error-handler"
|
||||
|
||||
/** Handle requests to /api/auth/signin */
|
||||
export default async function signin (req, res) {
|
||||
/**
|
||||
* Handle requests to /api/auth/signin
|
||||
* @param {import("types/internals").NextAuthRequest} req
|
||||
* @param {import("types/internals").NextAuthResponse} res
|
||||
*/
|
||||
export default async function signin(req, res) {
|
||||
const {
|
||||
provider,
|
||||
baseUrl,
|
||||
basePath,
|
||||
adapter,
|
||||
callbacks
|
||||
callbacks,
|
||||
logger,
|
||||
} = req.options
|
||||
|
||||
if (!provider.type) {
|
||||
return res.status(500).end(`Error: Type not specified for ${provider.name}`)
|
||||
}
|
||||
|
||||
if (provider.type === 'oauth' && req.method === 'POST') {
|
||||
if (provider.type === "oauth" && req.method === "POST") {
|
||||
try {
|
||||
const authorizationUrl = await getAuthorizationUrl(req)
|
||||
return res.redirect(authorizationUrl)
|
||||
} catch (error) {
|
||||
logger.error('SIGNIN_OAUTH_ERROR', error)
|
||||
logger.error("SIGNIN_OAUTH_ERROR", error)
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthSignin`)
|
||||
}
|
||||
} else if (provider.type === 'email' && req.method === 'POST') {
|
||||
} else if (provider.type === "email" && req.method === "POST") {
|
||||
if (!adapter) {
|
||||
logger.error('EMAIL_REQUIRES_ADAPTER_ERROR')
|
||||
logger.error("EMAIL_REQUIRES_ADAPTER_ERROR")
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
||||
}
|
||||
const { getUserByEmail } = await adapter.getAdapter(req.options)
|
||||
const { getUserByEmail } = adapterErrorHandler(
|
||||
await adapter.getAdapter(req.options),
|
||||
logger
|
||||
)
|
||||
|
||||
// Note: Technically the part of the email address local mailbox element
|
||||
// (everything before the @ symbol) should be treated as 'case sensitive'
|
||||
@@ -39,36 +47,43 @@ export default async function signin (req, res) {
|
||||
const email = req.body.email?.toLowerCase() ?? null
|
||||
|
||||
// If is an existing user return a user object (otherwise use placeholder)
|
||||
const profile = await getUserByEmail(email) || { email }
|
||||
const account = { id: provider.id, type: 'email', providerAccountId: email }
|
||||
const profile = (await getUserByEmail(email)) || { email }
|
||||
const account = { id: provider.id, type: "email", providerAccountId: email }
|
||||
|
||||
// Check if user is allowed to sign in
|
||||
try {
|
||||
const signInCallbackResponse = await callbacks.signIn(profile, account, { email, verificationRequest: true })
|
||||
const signInCallbackResponse = await callbacks.signIn(profile, account, {
|
||||
email,
|
||||
verificationRequest: true,
|
||||
})
|
||||
if (signInCallbackResponse === false) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
|
||||
} else if (typeof signInCallbackResponse === 'string') {
|
||||
} else if (typeof signInCallbackResponse === "string") {
|
||||
return res.redirect(signInCallbackResponse)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`
|
||||
)
|
||||
}
|
||||
// TODO: Remove in a future major release
|
||||
logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT')
|
||||
logger.warn("SIGNIN_CALLBACK_REJECT_REDIRECT")
|
||||
return res.redirect(error)
|
||||
}
|
||||
|
||||
try {
|
||||
await emailSignin(email, provider, req.options)
|
||||
} catch (error) {
|
||||
logger.error('SIGNIN_EMAIL_ERROR', error)
|
||||
logger.error("SIGNIN_EMAIL_ERROR", error)
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=EmailSignin`)
|
||||
}
|
||||
|
||||
return res.redirect(`${baseUrl}${basePath}/verify-request?provider=${encodeURIComponent(
|
||||
provider.id
|
||||
)}&type=${encodeURIComponent(provider.type)}`)
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/verify-request?provider=${encodeURIComponent(
|
||||
provider.id
|
||||
)}&type=${encodeURIComponent(provider.type)}`
|
||||
)
|
||||
}
|
||||
return res.redirect(`${baseUrl}${basePath}/signin`)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import * as cookie from '../lib/cookie'
|
||||
import logger from '../../lib/logger'
|
||||
import dispatchEvent from '../lib/dispatch-event'
|
||||
import * as cookie from "../lib/cookie"
|
||||
import dispatchEvent from "../lib/dispatch-event"
|
||||
import adapterErrorHandler from "../../adapters/error-handler"
|
||||
|
||||
/** Handle requests to /api/auth/signout */
|
||||
export default async function signout (req, res) {
|
||||
const { adapter, cookies, events, jwt, callbackUrl } = req.options
|
||||
/**
|
||||
* Handle requests to /api/auth/signout
|
||||
* @param {import("types/internals").NextAuthRequest} req
|
||||
* @param {import("types/internals").NextAuthResponse} res
|
||||
*/
|
||||
export default async function signout(req, res) {
|
||||
const { adapter, cookies, events, jwt, callbackUrl, logger } = req.options
|
||||
const useJwtSession = req.options.session.jwt
|
||||
const sessionToken = req.cookies[cookies.sessionToken.name]
|
||||
|
||||
@@ -18,7 +22,10 @@ export default async function signout (req, res) {
|
||||
}
|
||||
} else {
|
||||
// Get session from database
|
||||
const { getSession, deleteSession } = await adapter.getAdapter(req.options)
|
||||
const { getSession, deleteSession } = adapterErrorHandler(
|
||||
await adapter.getAdapter(req.options),
|
||||
logger
|
||||
)
|
||||
|
||||
try {
|
||||
// Dispatch signout event
|
||||
@@ -33,14 +40,14 @@ export default async function signout (req, res) {
|
||||
await deleteSession(sessionToken)
|
||||
} catch (error) {
|
||||
// If error, log it but continue
|
||||
logger.error('SIGNOUT_ERROR', error)
|
||||
logger.error("SIGNOUT_ERROR", error)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove Session Token
|
||||
cookie.set(res, cookies.sessionToken.name, '', {
|
||||
cookie.set(res, cookies.sessionToken.name, "", {
|
||||
...cookies.sessionToken.options,
|
||||
maxAge: 0
|
||||
maxAge: 0,
|
||||
})
|
||||
|
||||
return res.redirect(callbackUrl)
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
# To be able to run tests:
|
||||
# 1. copy to the root folder and rename to .env
|
||||
# 2. Populate with values
|
||||
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,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" ]
|
||||
@@ -1,52 +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: ./test/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
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user