mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
356 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb04ab4e76 | ||
|
|
07e2a83ccb | ||
|
|
065d9eb310 | ||
|
|
5da19f3c9a | ||
|
|
88ec3bad71 | ||
|
|
5ab7868533 | ||
|
|
835dda0899 | ||
|
|
ad4709764a | ||
|
|
55a2932973 | ||
|
|
49cb7e5bd7 | ||
|
|
b95182ded7 | ||
|
|
be28672fd4 | ||
|
|
e26c5fc905 | ||
|
|
543f812eb3 | ||
|
|
0c9f9777c5 | ||
|
|
34f334a71d | ||
|
|
172ad02f8c | ||
|
|
eed0001524 | ||
|
|
a2705fb5b9 | ||
|
|
cb1e5a7174 | ||
|
|
8cba5d06b5 | ||
|
|
c52ce57296 | ||
|
|
4dae822806 | ||
|
|
901f6fb189 | ||
|
|
bb2237d0f9 | ||
|
|
fab7ce8f94 | ||
|
|
2becdad990 | ||
|
|
e3c2c7756d | ||
|
|
718f2537cb | ||
|
|
ae26df091d | ||
|
|
1cbf73b2f6 | ||
|
|
46b62d723c | ||
|
|
457952bb5a | ||
|
|
17b789822d | ||
|
|
fd12194c0c | ||
|
|
1c662e9ddc | ||
|
|
968903d227 | ||
|
|
3dedf6c26c | ||
|
|
d1dbfe1023 | ||
|
|
63171a0271 | ||
|
|
872e180339 | ||
|
|
a7709df796 | ||
|
|
dbe283f0fa | ||
|
|
727426bbec | ||
|
|
5a3ee47337 | ||
|
|
8dd8f7c48a | ||
|
|
072c59d85a | ||
|
|
d0e8147a48 | ||
|
|
5bc8f8b986 | ||
|
|
136361e1f4 | ||
|
|
cc9869592c | ||
|
|
073da60c3d | ||
|
|
aacc34bbfd | ||
|
|
074688d10e | ||
|
|
b3ffe50c03 | ||
|
|
e6d063825d | ||
|
|
985f7b3431 | ||
|
|
237b016378 | ||
|
|
776b9480da | ||
|
|
07a3f76cb3 | ||
|
|
3726d68c49 | ||
|
|
e31db1726a | ||
|
|
a241199c11 | ||
|
|
5385ec20a9 | ||
|
|
810d02e671 | ||
|
|
e5535734f8 | ||
|
|
ba7aed1057 | ||
|
|
a7e08e2a32 | ||
|
|
0d13040264 | ||
|
|
582520f8ef | ||
|
|
95942519a5 | ||
|
|
f3e64f04cc | ||
|
|
ed5cc4aa65 | ||
|
|
0e20b60229 | ||
|
|
3aee24b5dc | ||
|
|
960ca85907 | ||
|
|
f960cc0f6f | ||
|
|
0f64f3eea7 | ||
|
|
71c78e8e24 | ||
|
|
d86609a2dc | ||
|
|
d0c3400d30 | ||
|
|
172e79cb04 | ||
|
|
46d5c76605 | ||
|
|
438efd8a9b | ||
|
|
d8d497cc91 | ||
|
|
6152c8afbb | ||
|
|
5ae6f6118c | ||
|
|
96ff048b59 | ||
|
|
e80f6e936d | ||
|
|
6b5a215fb2 | ||
|
|
782482b9f4 | ||
|
|
2d364f246a | ||
|
|
564b342f69 | ||
|
|
63638d81dc | ||
|
|
28683015f1 | ||
|
|
726c49603d | ||
|
|
a7113c6d3e | ||
|
|
910514c6e2 | ||
|
|
b7cca484cf | ||
|
|
e293e786a8 | ||
|
|
82dd6ba3e4 | ||
|
|
6e28a07746 | ||
|
|
61047e3c14 | ||
|
|
dc5f3f481d | ||
|
|
0343344802 | ||
|
|
134a95a4bd | ||
|
|
52a4bd97cd | ||
|
|
87d43e4038 | ||
|
|
68695af1f3 | ||
|
|
76df2b5e70 | ||
|
|
8bd9d87633 | ||
|
|
6af40e3fe2 | ||
|
|
cdc1ac52b2 | ||
|
|
f2a7ee0b34 | ||
|
|
d67f1b7718 | ||
|
|
396f5d8bbc | ||
|
|
d7e78d5996 | ||
|
|
ad3b0b6a7d | ||
|
|
f4a954ccbb | ||
|
|
93f051ce08 | ||
|
|
645b53ee49 | ||
|
|
9c9744f30a | ||
|
|
9860ad8c8c | ||
|
|
23ada52f97 | ||
|
|
65e4910618 | ||
|
|
30c6d6360f | ||
|
|
214b22ecbb | ||
|
|
f50ac194aa | ||
|
|
b40e1441ec | ||
|
|
90a8f7c890 | ||
|
|
a7bae0395b | ||
|
|
71b3122fd1 | ||
|
|
1caa9bb813 | ||
|
|
daed68aee2 | ||
|
|
2f3ed7507b | ||
|
|
a15bdc191b | ||
|
|
ea71a1fb2f | ||
|
|
0069095bce | ||
|
|
42a822c407 | ||
|
|
2cfe5ad879 | ||
|
|
cba149f4b6 | ||
|
|
31bb2c342c | ||
|
|
af30be1fa4 | ||
|
|
ebaa28f04e | ||
|
|
e26297b901 | ||
|
|
2562b3c5d8 | ||
|
|
f8e5a79ce1 | ||
|
|
be53ef0f71 | ||
|
|
fbb5a12cbd | ||
|
|
70a186c183 | ||
|
|
cff9d3e294 | ||
|
|
2865b8cc2d | ||
|
|
b84f1b681c | ||
|
|
ea1d09bf83 | ||
|
|
a18ec09307 | ||
|
|
37cb81094f | ||
|
|
040d7c5017 | ||
|
|
f53ea6c9a9 | ||
|
|
7f670c5222 | ||
|
|
b1a99ec32f | ||
|
|
ca065604a3 | ||
|
|
77de2abd14 | ||
|
|
f6d6c4344c | ||
|
|
e8b1513899 | ||
|
|
505efc8a5d | ||
|
|
76b983229a | ||
|
|
ecddaf696b | ||
|
|
b43e7dca43 | ||
|
|
e7c34fd74b | ||
|
|
e0dd8e400b | ||
|
|
21d22a7e08 | ||
|
|
cb2fe0cae6 | ||
|
|
9ed75c7788 | ||
|
|
8e8713755a | ||
|
|
7fdde6268e | ||
|
|
5e949a3b97 | ||
|
|
a979e040cd | ||
|
|
2205cfa754 | ||
|
|
0989ef6171 | ||
|
|
7979b1069e | ||
|
|
71b50082f8 | ||
|
|
f3cc4d1018 | ||
|
|
15570b7479 | ||
|
|
a5187b69e8 | ||
|
|
751fd7bb0e | ||
|
|
94054db3f3 | ||
|
|
f93dbbbfee | ||
|
|
e3fd0ad450 | ||
|
|
e06816a374 | ||
|
|
284118e708 | ||
|
|
84bcecbec1 | ||
|
|
5060bd7f9b | ||
|
|
16a8720b1d | ||
|
|
3c056d7ff5 | ||
|
|
49dd7a807d | ||
|
|
42596fbca5 | ||
|
|
68d0f9465a | ||
|
|
71b4af0894 | ||
|
|
8bbb0ec344 | ||
|
|
b2770d5a1f | ||
|
|
192e5bf07e | ||
|
|
9abdbb57eb | ||
|
|
989d23e827 | ||
|
|
025f33a91f | ||
|
|
6e2fc11d64 | ||
|
|
6b1b8613d0 | ||
|
|
6d023aa533 | ||
|
|
080dd5f569 | ||
|
|
a6867b3564 | ||
|
|
6750accc0a | ||
|
|
9aae7bbc54 | ||
|
|
a84fe596af | ||
|
|
18840ead40 | ||
|
|
f72ee5ec06 | ||
|
|
958c31a4ee | ||
|
|
f47f5c6c62 | ||
|
|
d5f5157366 | ||
|
|
9fbfd90bb2 | ||
|
|
97d6f19fab | ||
|
|
fdcc62bd26 | ||
|
|
4764d60268 | ||
|
|
5f51c975c9 | ||
|
|
0c9104c13a | ||
|
|
8a6f0944ef | ||
|
|
672cedc712 | ||
|
|
05b275956b | ||
|
|
219b01724b | ||
|
|
94b0c68c8f | ||
|
|
81ebff897f | ||
|
|
8ac14ed298 | ||
|
|
51cfec92ba | ||
|
|
78fd783bac | ||
|
|
cec46b0d67 | ||
|
|
7bedd4afa9 | ||
|
|
6e6a24a7af | ||
|
|
6f067be7b0 | ||
|
|
e4cc3a92e5 | ||
|
|
610ab39b40 | ||
|
|
7d1168637d | ||
|
|
b2c1f32057 | ||
|
|
9247495fee | ||
|
|
341fae28d4 | ||
|
|
b86ffa5dd5 | ||
|
|
5415a9c3ab | ||
|
|
dc516e8be8 | ||
|
|
29a0d9d295 | ||
|
|
5f5174f6e2 | ||
|
|
424b4ee257 | ||
|
|
545a7e752e | ||
|
|
c564b84182 | ||
|
|
0db233d208 | ||
|
|
5126f4e342 | ||
|
|
e09dfc6a7f | ||
|
|
ccfa1d55bb | ||
|
|
d7dc7b0753 | ||
|
|
0407e130c4 | ||
|
|
64084d634b | ||
|
|
438a737837 | ||
|
|
a482a64f10 | ||
|
|
2227d34725 | ||
|
|
9c6ef951a1 | ||
|
|
01c897f23e | ||
|
|
ea65d87d07 | ||
|
|
8d1e479d12 | ||
|
|
435b630849 | ||
|
|
773c74a756 | ||
|
|
6867bc92c8 | ||
|
|
eb6a4c46d9 | ||
|
|
cd3d2a138b | ||
|
|
0c356456bb | ||
|
|
6d44a34f7d | ||
|
|
7bda639361 | ||
|
|
40e453076e | ||
|
|
e065552784 | ||
|
|
a3104a009c | ||
|
|
e9eb6bc57e | ||
|
|
95e31b46af | ||
|
|
d5e70323f0 | ||
|
|
4e4d1eac28 | ||
|
|
15316f069e | ||
|
|
e6995d21cd | ||
|
|
433f096a63 | ||
|
|
9f487593fa | ||
|
|
65caaa6c4c | ||
|
|
0adfba8c5c | ||
|
|
2f0f738e2e | ||
|
|
1777a87be3 | ||
|
|
e94fd3b484 | ||
|
|
3b40335202 | ||
|
|
6d63b74db9 | ||
|
|
eb26722833 | ||
|
|
4937047d19 | ||
|
|
4305964864 | ||
|
|
91d93fb8fd | ||
|
|
e2e28fcfd0 | ||
|
|
66afc69a57 | ||
|
|
3046691119 | ||
|
|
88b87a53ff | ||
|
|
f1ae26efb6 | ||
|
|
ba83685a86 | ||
|
|
d514733f13 | ||
|
|
15cd608b19 | ||
|
|
08d7f5d778 | ||
|
|
a2ba7e9229 | ||
|
|
7c71a15699 | ||
|
|
351b804606 | ||
|
|
8f0501b7fe | ||
|
|
73d21e66dd | ||
|
|
6310311d52 | ||
|
|
d0caba1933 | ||
|
|
2f3291e48f | ||
|
|
43d8e3b894 | ||
|
|
5d4eb5d4e0 | ||
|
|
7ccdec22cb | ||
|
|
2ea64045cb | ||
|
|
daf97d298d | ||
|
|
ababc7ecdb | ||
|
|
33e72b2ae1 | ||
|
|
bf5716c674 | ||
|
|
c17a3b94f5 | ||
|
|
19a9c313e0 | ||
|
|
68043e65e4 | ||
|
|
a6ec60284d | ||
|
|
ff79c4b95b | ||
|
|
9c4e41a4c6 | ||
|
|
07ef3d59c5 | ||
|
|
4fe7162652 | ||
|
|
950a937633 | ||
|
|
1cc31def3e | ||
|
|
92f53c532b | ||
|
|
c6d6d9c002 | ||
|
|
85b859231c | ||
|
|
ea093dc0fc | ||
|
|
cd61178f44 | ||
|
|
eb53219cbd | ||
|
|
18d70ffbe9 | ||
|
|
bdcf823d26 | ||
|
|
3aeba2aa09 | ||
|
|
0793e2c8d8 | ||
|
|
0f01279c91 | ||
|
|
8fa9d00958 | ||
|
|
ab6ef8a19c | ||
|
|
8d68807bfe | ||
|
|
35fc38c328 | ||
|
|
85eeda5755 | ||
|
|
2e52c500a1 | ||
|
|
5886f9bea8 | ||
|
|
c497dcba26 | ||
|
|
493c45a864 | ||
|
|
b243b26a3d | ||
|
|
1d0749970a | ||
|
|
3474d3e250 | ||
|
|
a35c3a424c | ||
|
|
6e65ba87a6 | ||
|
|
ae7247f14f | ||
|
|
12a5d6b1f4 |
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
# Exclude directories we don't need from Docker context to improve build time
|
||||
node_modules
|
||||
www
|
||||
src
|
||||
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/types/ @balazsorban44 @lluia
|
||||
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# https://docs.github.com/en/github/administering-a-repository/displaying-a-sponsor-button-in-your-repository
|
||||
|
||||
github: [balazsorban44]
|
||||
12
.github/ISSUE_TEMPLATE/bug_report.md
vendored
12
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -6,23 +6,25 @@ assignees: ''
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the defect is.
|
||||
A clear and concise description of the bug in NextAuth.js.
|
||||
|
||||
**To Reproduce**
|
||||
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 example code (or link to public repository) which can be used to reproduce the behaviour.
|
||||
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.
|
||||
If applicable add screenshots or error logs to help explain the problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
**Documentation feedback**
|
||||
**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
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -5,13 +5,11 @@ labels: enhancement
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
*Please stick to one distinct feature request per issue where possible and raise additional feature quests as separate issues. Try to avoid adding feature requests to existing issues in the comments of issues raised by other users.*
|
||||
|
||||
**Summary of proposed feature**
|
||||
A clear and concise description of the feature being proposed.
|
||||
|
||||
**Purpose of proposed feature**
|
||||
A clear and concise description description of why this feature is necessary and what problems it solves.
|
||||
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).
|
||||
|
||||
12
.github/ISSUE_TEMPLATE/question.md
vendored
12
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -4,16 +4,18 @@ about: Ask a question about NextAuth.js or for help using it
|
||||
labels: question
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
*Please refer to the [documentation](https://next-auth.js.org/getting-started/introduction), the [example project](https://github.com/iaincollins/next-auth-example) and existing issues before creating a new issue.*
|
||||
<!-- 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.
|
||||
<!-- A clear and concise question. -->
|
||||
|
||||
**What are you trying to do**
|
||||
A description of what you are trying to do.
|
||||
<!-- A description of what you are trying to do, for context. -->
|
||||
|
||||
**Documentation feedback**
|
||||
**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
|
||||
|
||||
41
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
41
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
<!--
|
||||
Thanks for your interest in the project. Bugs filed and PRs submitted are appreciated!
|
||||
|
||||
Please make sure that you are familiar with and follow the Code of Conduct for
|
||||
this project (found in the CODE_OF_CONDUCT.md file).
|
||||
|
||||
Also, please make sure you're familiar with and follow the instructions in the
|
||||
contributing guidelines (found in the CONTRIBUTING.md file).
|
||||
|
||||
If you're new to contributing to open source projects, you might find this free
|
||||
video course helpful: https://kcd.im/pull-request
|
||||
|
||||
Please fill out the information below to expedite the review and (hopefully)
|
||||
merge of your pull request!
|
||||
-->
|
||||
|
||||
<!-- What changes are being made? (What feature/bug is being fixed here?) -->
|
||||
|
||||
**What**:
|
||||
|
||||
<!-- Why are these changes necessary? -->
|
||||
|
||||
**Why**:
|
||||
|
||||
<!-- How were these changes implemented? -->
|
||||
|
||||
**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" -->
|
||||
|
||||
- [ ] 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 -->
|
||||
39
.github/labeler.yml
vendored
Normal file
39
.github/labeler.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
test:
|
||||
- test/**/*
|
||||
- types/tests/**/*
|
||||
|
||||
documentation:
|
||||
- www/**/*
|
||||
- ./**/*.md
|
||||
|
||||
providers:
|
||||
- src/providers/**/*
|
||||
- www/docs/configuration/providers.md
|
||||
- test/integration/**/*
|
||||
|
||||
adapters:
|
||||
- src/adapters/**/*
|
||||
- www/docs/schemas/adapters.md
|
||||
|
||||
databases:
|
||||
- www/docs/schemas/*.md
|
||||
- test/docker/databases/**/*
|
||||
- www/docs/configuration/databases.md
|
||||
- test/fixtures/**/*
|
||||
|
||||
core:
|
||||
- src/**/*
|
||||
|
||||
style:
|
||||
- src/css/**/*
|
||||
|
||||
client:
|
||||
- src/client/**/*
|
||||
- www/docs/getting-started/client.md
|
||||
|
||||
pages:
|
||||
- src/server/pages/**/*
|
||||
- www/docs/configuration/pages.md
|
||||
|
||||
TypeScript:
|
||||
- types/**/*
|
||||
25
.github/stale.yml
vendored
Normal file
25
.github/stale.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 60
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- pinned
|
||||
- security
|
||||
- priority
|
||||
- bug
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
Hi there! It looks like this issue hasn't had any activity for a while.
|
||||
It will be closed if no further activity occurs. If you think your issue
|
||||
is still relevant, feel free to comment on it to keep it open. (Read more at #912)
|
||||
Thanks!
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: >
|
||||
Hi there! It looks like this issue hasn't had any activity for a while.
|
||||
To keep things tidy, I am going to close this issue for now.
|
||||
If you think your issue is still relevant, just leave a comment
|
||||
and I will reopen it. (Read more at #912)
|
||||
Thanks!
|
||||
32
.github/workflows/build.yml
vendored
Normal file
32
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
# 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
|
||||
67
.github/workflows/codeql-analysis.yml
vendored
Normal file
67
.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
# 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"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, beta, next ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main ]
|
||||
schedule:
|
||||
- cron: '43 17 * * 2'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
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
|
||||
|
||||
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
|
||||
57
.github/workflows/integration.yml
vendored
Normal file
57
.github/workflows/integration.yml
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
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}}
|
||||
11
.github/workflows/labeler.yml
vendored
Normal file
11
.github/workflows/labeler.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
name: "Pull Request Labeler"
|
||||
on:
|
||||
- pull_request_target
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@main
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
29
.github/workflows/node.js.yml
vendored
29
.github/workflows/node.js.yml
vendored
@@ -1,29 +0,0 @@
|
||||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: Node.js CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [10.x, 12.x, 14.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm ci
|
||||
- run: npm run build --if-present
|
||||
- run: npm test
|
||||
36
.github/workflows/npm-publish.yml
vendored
36
.github/workflows/npm-publish.yml
vendored
@@ -1,36 +0,0 @@
|
||||
# Publishes module to registry when a new release is created.
|
||||
#
|
||||
# The following secrets need to be configured for this workflow:
|
||||
#
|
||||
# * NPM_TOKEN - Auth token from npmjs.com
|
||||
|
||||
name: Publish to NPM
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
|
||||
publish-npm:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
registry-url: https://registry.npmjs.org/
|
||||
- run: npm ci
|
||||
- run: npm publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
||||
26
.github/workflows/release.yml
vendored
Normal file
26
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "beta"
|
||||
- "next"
|
||||
- "3.x"
|
||||
pull_request:
|
||||
jobs:
|
||||
release:
|
||||
name: "Release"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14
|
||||
- name: Install dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
- run: npx semantic-release@17
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
NPM_TOKEN: ${{secrets.NPM_TOKEN}}
|
||||
27
.github/workflows/types.yml
vendored
Normal file
27
.github/workflows/types.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
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
|
||||
64
.gitignore
vendored
64
.gitignore
vendored
@@ -1,20 +1,7 @@
|
||||
.next
|
||||
.env
|
||||
.vscode
|
||||
node_modules
|
||||
dist
|
||||
.DS_Store# Dependencies
|
||||
/node_modules
|
||||
|
||||
# Production
|
||||
/build
|
||||
|
||||
# Generated files
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
@@ -24,10 +11,51 @@ npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Docusaurus
|
||||
www/build
|
||||
yarn.lock
|
||||
|
||||
#VS
|
||||
# Dependencies
|
||||
node_modules
|
||||
|
||||
# Build dirs
|
||||
.next
|
||||
/build
|
||||
/dist
|
||||
/www/build
|
||||
|
||||
# Generated files
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
www/providers.json
|
||||
src/providers/index.js
|
||||
/internals
|
||||
/adapters.d.ts
|
||||
/adapters.js
|
||||
/client.d.ts
|
||||
/client.js
|
||||
/index.d.ts
|
||||
/index.js
|
||||
/jwt.d.ts
|
||||
/jwt.js
|
||||
/providers.d.ts
|
||||
/providers.js
|
||||
/errors.js
|
||||
/errors.d.ts
|
||||
|
||||
# Development app
|
||||
app/next-auth
|
||||
app/dist/css
|
||||
app/package-lock.json
|
||||
app/yarn.lock
|
||||
|
||||
# VS
|
||||
/.vs/slnx.sqlite-journal
|
||||
/.vs/slnx.sqlite
|
||||
/.vs
|
||||
.vscode
|
||||
|
||||
# GitHub Actions runner
|
||||
/actions-runner
|
||||
/_work
|
||||
|
||||
# Prisma migrations
|
||||
/prisma/migrations
|
||||
5
CHANGELOG.md
Normal file
5
CHANGELOG.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# CHANGELOG
|
||||
|
||||
The changelog is automatically updated using
|
||||
[semantic-release](https://github.com/semantic-release/semantic-release). You
|
||||
can see it on the [releases page](../../releases).
|
||||
155
CONTRIBUTING.md
155
CONTRIBUTING.md
@@ -2,108 +2,129 @@
|
||||
|
||||
Contributions and feedback on your experience of using this software are welcome.
|
||||
|
||||
This includes bug reports, feature requests, ideas, pull requests and examples of how you have used this software.
|
||||
This includes bug reports, feature requests, ideas, pull requests, and examples of how you have used this software.
|
||||
|
||||
Please see the [Code of Conduct](CODE_OF_CONDUCT.md) and follow any templates configured in GitHub when reporting bugs, requesting enhancements or contributing code.
|
||||
Please see the [Code of Conduct](CODE_OF_CONDUCT.md) and follow any templates configured in GitHub when reporting bugs, requesting enhancements, or contributing code.
|
||||
|
||||
Please raise any significant new functionality or breaking change an issue for discussion before raising a Pull Request for it.
|
||||
|
||||
## Pull Requests
|
||||
## For contributors
|
||||
|
||||
* The latest changes are always in `main`
|
||||
* Pull Requests should be raised for larger changes
|
||||
* Pull Requests do not need approval before merging for those with contributor access (it's just helpful to have them to track changes)
|
||||
* Rebasing in Pull Requests is prefered to keep a clean commit history (see below)
|
||||
* Running `npm run lint:fix` before committing can make resolving conflicts easier, but is not required
|
||||
* Merge commits (and pushing merge commits to `main`) are disabled in this repo; but commits in PR can be squashed so this is not a blocker
|
||||
* Pushing directly to main should ideally be reserved for minor updates (e.g. correcting typos) or small single-commit fixes
|
||||
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
|
||||
|
||||
## Rebasing
|
||||
* 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
|
||||
|
||||
*If you don't rebase and end up with merge commits in a PR then it's not a blocker, we can alway squash the commits when merging!*
|
||||
### Setting up local environment
|
||||
|
||||
If you create a branch and there are conflicting updates in the `main` branch, you can resolve them by rebasing from a check out of your branch:
|
||||
|
||||
git fetch
|
||||
git rebase origin/main
|
||||
|
||||
If there are any conflicts, you can resolve them and stage the files, then run:
|
||||
|
||||
git rebase --continue
|
||||
|
||||
*If there are a lot of changes you may be prompted to step more than once.*
|
||||
|
||||
When the rebase is complete (i.e. there are no more conflicts) you should push your changes to your branch before doing anyhing else:
|
||||
|
||||
git push --force-with-lease
|
||||
|
||||
You should see that any conflicts in your PR are now resolved. You can review changes to make sure it contains changes you intended to make.
|
||||
|
||||
*If you accidentally sync before pushing, it will trigger a merge. Uou can use `git merge --abort` to undo the merge.*
|
||||
|
||||
You can use `npm run lint:fix` to automatically apply Standard JS rules to resolve formatting differences (tabs vs spaces, line endings, etc).
|
||||
|
||||
## Setting up local environment
|
||||
|
||||
A quick and dirty 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:
|
||||
|
||||
1. Clone the repo:
|
||||
```sh
|
||||
git clone git@github.com:nextauthjs/next-auth.git
|
||||
cd next-auth
|
||||
```
|
||||
|
||||
git clone git@github.com:iaincollins/next-auth.git
|
||||
cd next-auth/
|
||||
2. Install packages:
|
||||
```sh
|
||||
npm i && npm dev:setup
|
||||
```
|
||||
|
||||
2. Install packages and run the build command:
|
||||
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.
|
||||
|
||||
npm i
|
||||
npm run build
|
||||
> 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`.
|
||||
|
||||
3. Link your project back to your local copy of next auth:
|
||||
1. Start the dev application/server:
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
cd ../your-application
|
||||
npm link ../next-auth
|
||||
Your dev application will be available on ```http://localhost:3000```
|
||||
|
||||
4. Finally link React between the repo and the version installed in your project:
|
||||
|
||||
cd ../next-auth
|
||||
npm link ../your-application/node_modules/react
|
||||
|
||||
*This is an annoying step and not obvious, but is needed because of how React has been written (otherwise React crashes when you try to use the `useSession()` hook in your project).*
|
||||
|
||||
That's it!
|
||||
|
||||
Notes: You may need to repeat both `npm link` steps if you install / update additional dependancies with `npm i`.
|
||||
That's it! 🎉
|
||||
|
||||
If you need an example project to link to, you can use [next-auth-example](https://github.com/iaincollins/next-auth-example).
|
||||
|
||||
### Hot reloading
|
||||
#### Hot reloading
|
||||
|
||||
You might find it helpful to use the `npm run watch` command in the next-auth project, which will automatically (and silently) rebuild JS and CSS files as you edit them.
|
||||
When running `npm run dev`, you start a Next.js dev server on `http://localhost:3000`, which includes hot reloading out of the box. Make changes on any of the files in `src` and see the changes immediately.
|
||||
|
||||
cd next-auth/
|
||||
npm run watch
|
||||
> NOTE: When working on CSS, you will have to manually refresh the page after changes. The reason for this is our pages using CSS are server-side rendered. (Improving this through a PR is very welcome!)
|
||||
|
||||
If you are working on `next-auth/src/client/index.js` hot reloading will work as normal in your Next.js app.
|
||||
> NOTE: The setup is as follows: The development application lives inside the `app` folder, and whenever you make a change to the `src` folder in the root (where next-auth is), it gets copied into `app` every time (gitignored), so Next.js can pick them up and apply hot reloading. This is to avoid some annoying issues with how symlinks are working with different React builds, and also to provide a super-fast feedback loop while developing core features.
|
||||
|
||||
However if you are working on anything else (e.g. `next-auth/src/server/*` etc) then you will need to *stop and start* your app for changes to apply as **Next.js will not hot reload those changes**.
|
||||
#### Providers
|
||||
|
||||
### Databases
|
||||
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)
|
||||
|
||||
Included is a Docker Compose file that starts up MySQL, Postgres and MongoDB databases on localhost.
|
||||
That's it! 🎉 Others will be able to discover this provider much more easily now!
|
||||
|
||||
It will use port 3306, 5432 and 27017 on localhost respectively; it will not work if are running existing databases on localhost.
|
||||
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 installed to be able to start / stop the databases.
|
||||
You will need Docker and Docker Compose installed to be able to start / stop the databases.
|
||||
|
||||
When stop the databases, it will reset their contents.
|
||||
When stopping the databases, it will reset their contents.
|
||||
|
||||
### Testing
|
||||
#### 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`).
|
||||
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.
|
||||
|
||||
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.)
|
||||
|
||||
### 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.
|
||||
|
||||
Some recommened 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.
|
||||
|
||||
|
||||
### Skipping a release
|
||||
|
||||
Every commit that contains [skip release] or [release skip] in their message will be excluded from the commit analysis and won't participate in the release type determination. This is useful, if the PR being merged should not trigger a new `npm` release.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2018-2020, Iain Collins
|
||||
Copyright (c) 2018-2021, Iain Collins
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
107
README.md
107
README.md
@@ -1,4 +1,33 @@
|
||||
# NextAuth.js
|
||||
<p align="center">
|
||||
<br/>
|
||||
<a href="https://next-auth.js.org" target="_blank"><img width="150px" src="https://next-auth.js.org/img/logo/logo-sm.png" /></a>
|
||||
<h3 align="center">NextAuth.js</h3>
|
||||
<p align="center">Authentication for Next.js</p>
|
||||
<p align="center">
|
||||
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>
|
||||
<a href="https://bundlephobia.com/result?p=next-auth">
|
||||
<img src="https://img.shields.io/bundlephobia/minzip/next-auth" alt="Bundle Size"/>
|
||||
</a>
|
||||
<a href="https://www.npmtrends.com/next-auth">
|
||||
<img src="https://img.shields.io/npm/dm/next-auth" alt="Downloads" />
|
||||
</a>
|
||||
<a href="https://github.com/nextauthjs/next-auth/stargazers">
|
||||
<img src="https://img.shields.io/github/stars/nextauthjs/next-auth" alt="Github Stars" />
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/next-auth">
|
||||
<img src="https://img.shields.io/github/v/release/nextauthjs/next-auth?label=latest" alt="Github Stable Release" />
|
||||
</a>
|
||||
<img src="https://img.shields.io/github/v/release/nextauthjs/next-auth?include_prereleases&label=prerelease&sort=semver" alt="Github Prelease" />
|
||||
</p>
|
||||
</p>
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -6,9 +35,15 @@ NextAuth.js is a complete open source authentication solution for [Next.js](http
|
||||
|
||||
It is designed from the ground up to support Next.js and Serverless.
|
||||
|
||||
[Follow the examples](https://next-auth.js.org/getting-started/example) to see how easy it is to use NextAuth.js for authentication.
|
||||
## Getting Started
|
||||
|
||||
Install: `npm i next-auth`
|
||||
```
|
||||
npm install --save next-auth
|
||||
```
|
||||
|
||||
The easiest way to continue getting started, is to follow the [getting started](https://next-auth.js.org/getting-started/example) section in our docs.
|
||||
|
||||
We also have a section of [tutorials](https://next-auth.js.org/tutorials) for those looking for more specific examples.
|
||||
|
||||
See [next-auth.js.org](https://next-auth.js.org) for more information and documentation.
|
||||
|
||||
@@ -29,7 +64,7 @@ NextAuth.js can be used with or without a database.
|
||||
|
||||
* An open source solution that allows you to keep control of your data
|
||||
* Supports Bring Your Own Database (BYOD) and can be used with any database
|
||||
* Built-in support for [MySQL, MariaDB, Postgres, MongoDB and SQLite](https://next-auth.js.org/configuration/databases)
|
||||
* Built-in support for [MySQL, MariaDB, Postgres, Microsoft SQL Server, MongoDB and SQLite](https://next-auth.js.org/configuration/databases)
|
||||
* Works great with databases from popular hosting providers
|
||||
* Can also be used *without a database* (e.g. OAuth + JWT)
|
||||
|
||||
@@ -47,6 +82,12 @@ NextAuth.js can be used with or without a database.
|
||||
|
||||
Advanced options allow you to define your own routines to handle controlling what accounts are allowed to sign in, for encoding and decoding JSON Web Tokens and to set custom cookie security policies and session properties, so you can control who is able to sign in and how often sessions have to be re-validated.
|
||||
|
||||
### 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.
|
||||
|
||||
The package at `@types/next-auth` is now deprecated.
|
||||
|
||||
## Example
|
||||
|
||||
### Add API Route
|
||||
@@ -55,7 +96,7 @@ Advanced options allow you to define your own routines to handle controlling wha
|
||||
import NextAuth from 'next-auth'
|
||||
import Providers from 'next-auth/providers'
|
||||
|
||||
const options = {
|
||||
export default NextAuth({
|
||||
providers: [
|
||||
// OAuth authentication providers
|
||||
Providers.Apple({
|
||||
@@ -74,45 +115,51 @@ const options = {
|
||||
],
|
||||
// SQL or MongoDB database (or leave empty)
|
||||
database: process.env.DATABASE_URL
|
||||
}
|
||||
|
||||
export default (req, res) => NextAuth(req, res, options)
|
||||
})
|
||||
```
|
||||
|
||||
### Add React Component
|
||||
|
||||
```javascript
|
||||
import React from 'react'
|
||||
import {
|
||||
useSession,
|
||||
signin,
|
||||
signout
|
||||
import {
|
||||
useSession, signIn, signOut
|
||||
} from 'next-auth/client'
|
||||
|
||||
export default () => {
|
||||
export default function Component() {
|
||||
const [ session, loading ] = useSession()
|
||||
|
||||
return <p>
|
||||
{!session && <>
|
||||
Not signed in <br/>
|
||||
<button onClick={signin}>Sign in</button>
|
||||
</>}
|
||||
{session && <>
|
||||
if(session) {
|
||||
return <>
|
||||
Signed in as {session.user.email} <br/>
|
||||
<button onClick={signout}>Sign out</button>
|
||||
</>}
|
||||
</p>
|
||||
<button onClick={() => signOut()}>Sign out</button>
|
||||
</>
|
||||
}
|
||||
return <>
|
||||
Not signed in <br/>
|
||||
<button onClick={() => signIn()}>Sign in</button>
|
||||
</>
|
||||
}
|
||||
```
|
||||
|
||||
## Acknowledgement
|
||||
## Acknowledgements
|
||||
|
||||
[NextAuth.js is possible thanks to its contributors.](https://next-auth.js.org/contributors)
|
||||
[NextAuth.js is made possible thanks to all of its contributors.](https://next-auth.js.org/contributors)
|
||||
|
||||
## Getting started
|
||||
|
||||
[Follow the examples to get started.](https://next-auth.js.org/getting-started/example)
|
||||
<a href="https://github.com/nextauthjs/next-auth/graphs/contributors">
|
||||
<img width="500px" src="https://contrib.rocks/image?repo=nextauthjs/next-auth" />
|
||||
</a>
|
||||
<div>
|
||||
<a href="https://vercel.com?utm_source=nextauthjs&utm_campaign=oss">
|
||||
<img width="170px" src="https://raw.githubusercontent.com/nextauthjs/next-auth/canary/www/static/img/powered-by-vercel.svg" alt="Powered By Vercel" />
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<p align="left">Thanks to Vercel sponsoring this project by allowing it to be deployed for free for the entire NextAuth.js Team</p>
|
||||
</div>
|
||||
|
||||
## Contributing
|
||||
|
||||
If you'd like to contribute to you can find useful information in our [Contributing Guide](https://github.com/iaincollins/next-auth/blob/main/CONTRIBUTING.md).
|
||||
We're open to all community contributions! If you'd like to contribute in any way, please first read our [Contributing Guide](https://github.com/nextauthjs/next-auth/blob/canary/CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
ISC
|
||||
|
||||
24
SECURITY.md
Normal file
24
SECURITY.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Security Policy
|
||||
|
||||
NextAuth.js practices responsible disclosure.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Security updates are only released for the current version.
|
||||
|
||||
Old releases are not maintained and do not receive updates.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
We request that you contact us directly to report serious issues that might impact the security of sites using NextAuth.js.
|
||||
|
||||
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.
|
||||
|
||||
Currently, the best way to report an issue is by emailing me@iaincollins.com
|
||||
|
||||
For less serious issues (e.g. RFC compliance for unsupported flows or potential issues that may cause a problem future or default behaviour / options) it is appropriate to submit these these publically as bug reports or feature requests or to raise a question to open a discussion around them.
|
||||
@@ -1 +0,0 @@
|
||||
module.exports = require('./dist/adapters').default
|
||||
33
app/.env.local.example
Normal file
33
app/.env.local.example
Normal file
@@ -0,0 +1,33 @@
|
||||
# Rename file to .env.local (or .env) and populate values
|
||||
# to be able to run the dev app
|
||||
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
|
||||
# You can use `openssl rand -hex 32` or
|
||||
# https://generate-secret.now.sh/32 to generate a secret.
|
||||
# Note: Changing a secret may invalidate existing sessions
|
||||
# and/or verificaion tokens.
|
||||
SECRET=
|
||||
|
||||
AUTH0_ID=
|
||||
AUTH0_DOMAIN=
|
||||
AUTH0_SECRET=
|
||||
|
||||
GITHUB_ID=
|
||||
GITHUB_SECRET=
|
||||
|
||||
TWITTER_ID=
|
||||
TWITTER_SECRET=
|
||||
|
||||
# Example configuration for a Gmail account (will need SMTP enabled)
|
||||
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
|
||||
# MySQL: DATABASE_URL=mysql://nextauth:password@127.0.0.1:3306/nextauth?synchronize=true
|
||||
# MongoDB: DATABASE_URL=mongodb://nextauth:password@127.0.0.1:27017/nextauth?synchronize=true
|
||||
DATABASE_URL=
|
||||
6
app/README.md
Normal file
6
app/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# NextAuth.js Development App
|
||||
|
||||
This folder contains a Next.js app using NextAuth.js for local development. See the following section on how to start:
|
||||
|
||||
[Setting up local environment
|
||||
](https://github.com/nextauthjs/next-auth/blob/main/CONTRIBUTING.md#setting-up-local-environment)
|
||||
19
app/components/access-denied.js
Normal file
19
app/components/access-denied.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { signIn } from 'next-auth/client'
|
||||
|
||||
export default function AccessDenied () {
|
||||
return (
|
||||
<>
|
||||
<h1>Access Denied</h1>
|
||||
<p>
|
||||
<a
|
||||
href='/api/auth/signin'
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
signIn()
|
||||
}}
|
||||
>You must be signed in to view this page
|
||||
</a>
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
18
app/components/footer.js
Normal file
18
app/components/footer.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import Link from 'next/link'
|
||||
import styles from './footer.module.css'
|
||||
import { version } from 'package.json'
|
||||
|
||||
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>
|
||||
</ul>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
14
app/components/footer.module.css
Normal file
14
app/components/footer.module.css
Normal file
@@ -0,0 +1,14 @@
|
||||
.footer {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.navItems {
|
||||
margin-bottom: 1rem;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.navItem {
|
||||
display: inline-block;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
112
app/components/header.js
Normal file
112
app/components/header.js
Normal file
@@ -0,0 +1,112 @@
|
||||
import Link from 'next/link'
|
||||
import { signIn, signOut, useSession } from 'next-auth/client'
|
||||
import styles from './header.module.css'
|
||||
|
||||
// The approach used in this component shows how to built a sign in and sign out
|
||||
// component that works on pages which support both client and server side
|
||||
// rendering, and avoids any flash incorrect content on initial page load.
|
||||
export default function Header () {
|
||||
const [session, loading] = useSession()
|
||||
|
||||
return (
|
||||
<header>
|
||||
<noscript>
|
||||
<style>{'.nojs-show { opacity: 1; top: 0; }'}</style>
|
||||
</noscript>
|
||||
<div className={styles.signedInStatus}>
|
||||
<p
|
||||
className={`nojs-show ${
|
||||
!session && loading ? styles.loading : styles.loaded
|
||||
}`}
|
||||
>
|
||||
{!session && (
|
||||
<>
|
||||
<span className={styles.notSignedInText}>
|
||||
You are not signed in
|
||||
</span>
|
||||
<a
|
||||
href='/api/auth/signin'
|
||||
className={styles.buttonPrimary}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
signIn()
|
||||
}}
|
||||
>
|
||||
Sign in
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
{session && (
|
||||
<>
|
||||
{session.user.image && (
|
||||
<span
|
||||
style={{ backgroundImage: `url(${session.user.image})` }}
|
||||
className={styles.avatar}
|
||||
/>
|
||||
)}
|
||||
<span className={styles.signedInText}>
|
||||
<small>Signed in as</small>
|
||||
<br />
|
||||
<strong>{session.user.email || session.user.name}</strong>
|
||||
</span>
|
||||
<a
|
||||
href='/api/auth/signout'
|
||||
className={styles.button}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
signOut()
|
||||
}}
|
||||
>
|
||||
Sign out
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<nav>
|
||||
<ul className={styles.navItems}>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/'>
|
||||
<a>Home</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/client'>
|
||||
<a>Client</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/server'>
|
||||
<a>Server</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/protected'>
|
||||
<a>Protected</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/protected-ssr'>
|
||||
<a>Protected(SSR)</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/api-example'>
|
||||
<a>API</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/credentials'>
|
||||
<a>Credentials</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/email'>
|
||||
<a>Email</a>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
92
app/components/header.module.css
Normal file
92
app/components/header.module.css
Normal file
@@ -0,0 +1,92 @@
|
||||
/* Set min-height to avoid page reflow while session loading */
|
||||
.signedInStatus {
|
||||
display: block;
|
||||
min-height: 4rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.loaded {
|
||||
position: relative;
|
||||
top: 0;
|
||||
opacity: 1;
|
||||
overflow: hidden;
|
||||
border-radius: 0 0 .6rem .6rem;
|
||||
padding: .6rem 1rem;
|
||||
margin: 0;
|
||||
background-color: rgba(0,0,0,.05);
|
||||
transition: all 0.2s ease-in;
|
||||
}
|
||||
|
||||
.loading {
|
||||
top: -2rem;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.signedInText,
|
||||
.notSignedInText {
|
||||
position: absolute;
|
||||
padding-top: .8rem;
|
||||
left: 1rem;
|
||||
right: 6.5rem;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
display: inherit;
|
||||
z-index: 1;
|
||||
line-height: 1.3rem;
|
||||
}
|
||||
|
||||
.signedInText {
|
||||
padding-top: 0rem;
|
||||
left: 4.6rem;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border-radius: 2rem;
|
||||
float: left;
|
||||
height: 2.8rem;
|
||||
width: 2.8rem;
|
||||
background-color: white;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.button,
|
||||
.buttonPrimary {
|
||||
float: right;
|
||||
margin-right: -.4rem;
|
||||
font-weight: 500;
|
||||
border-radius: .3rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
line-height: 1.4rem;
|
||||
padding: .7rem .8rem;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
background-color: transparent;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.buttonPrimary {
|
||||
background-color: #346df1;
|
||||
border-color: #346df1;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
padding: .7rem 1.4rem;
|
||||
}
|
||||
|
||||
.buttonPrimary:hover {
|
||||
box-shadow: inset 0 0 5rem rgba(0,0,0,0.2)
|
||||
}
|
||||
|
||||
.navItems {
|
||||
margin-bottom: 2rem;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.navItem {
|
||||
display: inline-block;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
14
app/components/layout.js
Normal file
14
app/components/layout.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import Header from 'components/header'
|
||||
import Footer from 'components/footer'
|
||||
|
||||
export default function Layout ({ children }) {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main>
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
)
|
||||
}
|
||||
5
app/jsconfig.json
Normal file
5
app/jsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "."
|
||||
}
|
||||
}
|
||||
2
app/next-env.d.ts
vendored
Normal file
2
app/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
||||
19
app/next.config.js
Normal file
19
app/next.config.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const path = require("path")
|
||||
|
||||
module.exports = {
|
||||
webpack(config) {
|
||||
config.resolve = {
|
||||
...config.resolve,
|
||||
alias: {
|
||||
...config.resolve.alias,
|
||||
"next-auth$": path.join(process.cwd(), "next-auth/server"),
|
||||
"next-auth/client$": path.join(process.cwd(), "next-auth/client"),
|
||||
"next-auth/jwt$": path.join(process.cwd(), "next-auth/lib/jwt"),
|
||||
"next-auth/adapters": path.join(process.cwd(), "next-auth/adapters"),
|
||||
"next-auth/providers": path.join(process.cwd(), "next-auth/providers"),
|
||||
},
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
}
|
||||
25
app/package.json
Normal file
25
app/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "next-auth-app",
|
||||
"version": "1.0.0",
|
||||
"description": "NextAuth.js Developer app",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "npm-run-all --parallel copy:app dev:css dev:next",
|
||||
"dev:next": "next dev",
|
||||
"copy:app": "cpx \"../src/**/*\" next-auth --watch",
|
||||
"copy:css": "cpx \"../dist/css/**/*\" dist/css --watch",
|
||||
"watch:css": "cd .. && npm run watch:css",
|
||||
"dev:css": "npm-run-all --parallel watch:css copy:css",
|
||||
"start": "next start"
|
||||
},
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"next": "^10.1.3",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cpx": "^1.5.0",
|
||||
"npm-run-all": "^4.1.5"
|
||||
}
|
||||
}
|
||||
31
app/pages/_app.js
Normal file
31
app/pages/_app.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Provider } from "next-auth/client"
|
||||
import "./styles.css"
|
||||
|
||||
// Use the <Provider> to improve performance and allow components that call
|
||||
// `useSession()` anywhere in your application to access the `session` object.
|
||||
export default function App({ Component, pageProps }) {
|
||||
return (
|
||||
<Provider
|
||||
// Provider options are not required but can be useful in situations where
|
||||
// you have a short session maxAge time. Shown here with default values.
|
||||
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>
|
||||
)
|
||||
}
|
||||
17
app/pages/api-example.js
Normal file
17
app/pages/api-example.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import Layout from '../components/layout'
|
||||
|
||||
export default function Page () {
|
||||
return (
|
||||
<Layout>
|
||||
<h1>API Example</h1>
|
||||
<p>The examples below show responses from the example API endpoints.</p>
|
||||
<p><em>You must be signed in to see responses.</em></p>
|
||||
<h2>Session</h2>
|
||||
<p>/api/examples/session</p>
|
||||
<iframe src='/api/examples/session' />
|
||||
<h2>JSON Web Token</h2>
|
||||
<p>/api/examples/jwt</p>
|
||||
<iframe src='/api/examples/jwt' />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
87
app/pages/api/auth/[...nextauth].js
Normal file
87
app/pages/api/auth/[...nextauth].js
Normal file
@@ -0,0 +1,87 @@
|
||||
import NextAuth from 'next-auth'
|
||||
import Providers from 'next-auth/providers'
|
||||
|
||||
// import Adapters from 'next-auth/adapters'
|
||||
// import { PrismaClient } from '@prisma/client'
|
||||
// const prisma = new PrismaClient()
|
||||
|
||||
export default NextAuth({
|
||||
// Used to debug https://github.com/nextauthjs/next-auth/issues/1664
|
||||
// cookies: {
|
||||
// csrfToken: {
|
||||
// name: 'next-auth.csrf-token',
|
||||
// options: {
|
||||
// httpOnly: true,
|
||||
// sameSite: 'none',
|
||||
// path: '/',
|
||||
// secure: true
|
||||
// }
|
||||
// },
|
||||
// pkceCodeVerifier: {
|
||||
// name: 'next-auth.pkce.code_verifier',
|
||||
// options: {
|
||||
// httpOnly: true,
|
||||
// sameSite: 'none',
|
||||
// path: '/',
|
||||
// secure: true
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
providers: [
|
||||
Providers.Email({
|
||||
server: process.env.EMAIL_SERVER,
|
||||
from: process.env.EMAIL_FROM
|
||||
}),
|
||||
Providers.GitHub({
|
||||
clientId: process.env.GITHUB_ID,
|
||||
clientSecret: process.env.GITHUB_SECRET
|
||||
}),
|
||||
Providers.Auth0({
|
||||
clientId: process.env.AUTH0_ID,
|
||||
clientSecret: process.env.AUTH0_SECRET,
|
||||
domain: process.env.AUTH0_DOMAIN,
|
||||
// Used to debug https://github.com/nextauthjs/next-auth/issues/1664
|
||||
// protection: ["pkce", "state"],
|
||||
// authorizationParams: {
|
||||
// response_mode: 'form_post'
|
||||
// }
|
||||
protection: 'pkce'
|
||||
}),
|
||||
Providers.Twitter({
|
||||
clientId: process.env.TWITTER_ID,
|
||||
clientSecret: process.env.TWITTER_SECRET
|
||||
}),
|
||||
Providers.Credentials({
|
||||
name: 'Credentials',
|
||||
credentials: {
|
||||
password: { label: 'Password', type: 'password' }
|
||||
},
|
||||
async authorize (credentials) {
|
||||
if (credentials.password === 'password') {
|
||||
return {
|
||||
id: 1,
|
||||
name: 'Fill Murray',
|
||||
email: 'bill@fillmurray.com',
|
||||
image: 'https://www.fillmurray.com/64/64'
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
})
|
||||
],
|
||||
jwt: {
|
||||
encryption: true,
|
||||
secret: process.env.SECRET
|
||||
},
|
||||
debug: false,
|
||||
theme: 'auto'
|
||||
|
||||
// Default Database Adapter (TypeORM)
|
||||
// database: process.env.DATABASE_URL
|
||||
|
||||
// Prisma Database Adapter
|
||||
// To configure this app to use the schema in `prisma/schema.prisma` run:
|
||||
// npx prisma generate
|
||||
// npx prisma migrate dev
|
||||
// adapter: Adapters.Prisma.Adapter({ prisma })
|
||||
})
|
||||
9
app/pages/api/examples/jwt.js
Normal file
9
app/pages/api/examples/jwt.js
Normal file
@@ -0,0 +1,9 @@
|
||||
// This is an example of how to read a JSON Web Token from an API route
|
||||
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))
|
||||
}
|
||||
12
app/pages/api/examples/protected.js
Normal file
12
app/pages/api/examples/protected.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// This is an example of to protect an API route
|
||||
import { getSession } from 'next-auth/client'
|
||||
|
||||
export default async (req, res) => {
|
||||
const session = await getSession({ req })
|
||||
|
||||
if (session) {
|
||||
res.send({ content: 'This is protected content. You can access this content because you are signed in.' })
|
||||
} else {
|
||||
res.send({ error: 'You must be sign in to view the protected content on this page.' })
|
||||
}
|
||||
}
|
||||
7
app/pages/api/examples/session.js
Normal file
7
app/pages/api/examples/session.js
Normal file
@@ -0,0 +1,7 @@
|
||||
// This is an example of how to access a session from an API route
|
||||
import { getSession } from 'next-auth/client'
|
||||
|
||||
export default async (req, res) => {
|
||||
const session = await getSession({ req })
|
||||
res.send(JSON.stringify(session, null, 2))
|
||||
}
|
||||
22
app/pages/client.js
Normal file
22
app/pages/client.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import Layout from '../components/layout'
|
||||
|
||||
export default function Page () {
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Client Side Rendering</h1>
|
||||
<p>
|
||||
This page uses the <strong>useSession()</strong> React Hook in the <strong></Header></strong> component.
|
||||
</p>
|
||||
<p>
|
||||
The <strong>useSession()</strong> React Hook easy to use and allows pages to render very quickly.
|
||||
</p>
|
||||
<p>
|
||||
The advantage of this approach is that session state is shared between pages by using the <strong>Provider</strong> in <strong>_app.js</strong> so
|
||||
that navigation between pages using <strong>useSession()</strong> is very fast.
|
||||
</p>
|
||||
<p>
|
||||
The disadvantage of <strong>useSession()</strong> is that it requires client side JavaScript.
|
||||
</p>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
53
app/pages/credentials.js
Normal file
53
app/pages/credentials.js
Normal file
@@ -0,0 +1,53 @@
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
import * as React from 'react'
|
||||
import { signIn, signOut, useSession } from 'next-auth/client'
|
||||
import Layout from 'components/layout'
|
||||
|
||||
export default function Page () {
|
||||
const [response, setResponse] = React.useState(null)
|
||||
const handleLogin = (options) => async () => {
|
||||
if (options.redirect) {
|
||||
return signIn('credentials', options)
|
||||
}
|
||||
const response = await signIn('credentials', options)
|
||||
setResponse(response)
|
||||
}
|
||||
|
||||
const handleLogout = (options) => async () => {
|
||||
if (options.redirect) {
|
||||
return signOut(options)
|
||||
}
|
||||
const response = await signOut(options)
|
||||
setResponse(response)
|
||||
}
|
||||
|
||||
const [session] = useSession()
|
||||
|
||||
if (session) {
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Test different flows for Credentials logout</h1>
|
||||
<span className='spacing'>Default:</span>
|
||||
<button onClick={handleLogout({ redirect: true })}>Logout</button><br />
|
||||
<span className='spacing'>No redirect:</span>
|
||||
<button onClick={handleLogout({ redirect: false })}>Logout</button><br />
|
||||
<p>Response:</p>
|
||||
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Test different flows for Credentials login</h1>
|
||||
<span className='spacing'>Default:</span>
|
||||
<button onClick={handleLogin({ redirect: true, password: 'password' })}>Login</button><br />
|
||||
<span className='spacing'>No redirect:</span>
|
||||
<button onClick={handleLogin({ redirect: false, password: 'password' })}>Login</button><br />
|
||||
<span className='spacing'>No redirect, wrong password:</span>
|
||||
<button onClick={handleLogin({ redirect: false, password: '' })}>Login</button>
|
||||
<p>Response:</p>
|
||||
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
67
app/pages/email.js
Normal file
67
app/pages/email.js
Normal file
@@ -0,0 +1,67 @@
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
import * as React from 'react'
|
||||
import { signIn, signOut, useSession } from 'next-auth/client'
|
||||
import Layout from 'components/layout'
|
||||
|
||||
export default function Page () {
|
||||
const [response, setResponse] = React.useState(null)
|
||||
const [email, setEmail] = React.useState('')
|
||||
|
||||
const handleChange = (event) => {
|
||||
setEmail(event.target.value)
|
||||
}
|
||||
|
||||
const handleLogin = (options) => async (event) => {
|
||||
event.preventDefault()
|
||||
|
||||
if (options.redirect) {
|
||||
return signIn('email', options)
|
||||
}
|
||||
const response = await signIn('email', options)
|
||||
setResponse(response)
|
||||
}
|
||||
|
||||
const handleLogout = (options) => async (event) => {
|
||||
if (options.redirect) {
|
||||
return signOut(options)
|
||||
}
|
||||
const response = await signOut(options)
|
||||
setResponse(response)
|
||||
}
|
||||
|
||||
const [session] = useSession()
|
||||
|
||||
if (session) {
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Test different flows for Email logout</h1>
|
||||
<span className='spacing'>Default:</span>
|
||||
<button onClick={handleLogout({ redirect: true })}>Logout</button><br />
|
||||
<span className='spacing'>No redirect:</span>
|
||||
<button onClick={handleLogout({ redirect: false })}>Logout</button><br />
|
||||
<p>Response:</p>
|
||||
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Test different flows for Email login</h1>
|
||||
<label className='spacing'>
|
||||
Email address:{' '}
|
||||
<input type='text' id='email' name='email' value={email} onChange={handleChange} />
|
||||
</label><br />
|
||||
<form onSubmit={handleLogin({ redirect: true, email })}>
|
||||
<span className='spacing'>Default:</span>
|
||||
<button type='submit'>Sign in with Email</button>
|
||||
</form>
|
||||
<form onSubmit={handleLogin({ redirect: false, email })}>
|
||||
<span className='spacing'>No redirect:</span>
|
||||
<button type='submit'>Sign in with Email</button>
|
||||
</form>
|
||||
<p>Response:</p>
|
||||
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
12
app/pages/index.js
Normal file
12
app/pages/index.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import Layout from 'components/layout'
|
||||
|
||||
export default function Page () {
|
||||
return (
|
||||
<Layout>
|
||||
<h1>NextAuth.js Example</h1>
|
||||
<p>
|
||||
This is an example site to demonstrate how to use <a href='https://next-auth.js.org'>NextAuth.js</a> for authentication.
|
||||
</p>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
30
app/pages/policy.js
Normal file
30
app/pages/policy.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import Layout from '../components/layout'
|
||||
|
||||
export default function Page () {
|
||||
return (
|
||||
<Layout>
|
||||
<p>
|
||||
This is an example site to demonstrate how to use <a href='https://next-auth.js.org'>NextAuth.js</a> for authentication.
|
||||
</p>
|
||||
<h2>Terms of Service</h2>
|
||||
<p>
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
</p>
|
||||
<h2>Privacy Policy</h2>
|
||||
<p>
|
||||
This site uses JSON Web Tokens and an in-memory database which resets every ~2 hours.
|
||||
</p>
|
||||
<p>
|
||||
Data provided to this site is exclusively used to support signing in
|
||||
and is not passed to any third party services, other than via SMTP or OAuth for the
|
||||
purposes of authentication.
|
||||
</p>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
37
app/pages/protected-ssr.js
Normal file
37
app/pages/protected-ssr.js
Normal file
@@ -0,0 +1,37 @@
|
||||
// This is an example of how to protect content using server rendering
|
||||
import { getSession } from 'next-auth/client'
|
||||
import Layout from '../components/layout'
|
||||
import AccessDenied from '../components/access-denied'
|
||||
|
||||
export default function Page ({ content, session }) {
|
||||
// If no session exists, display access denied message
|
||||
if (!session) { return <Layout><AccessDenied /></Layout> }
|
||||
|
||||
// If session exists, display content
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Protected Page</h1>
|
||||
<p><strong>{content}</strong></p>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export async function getServerSideProps (context) {
|
||||
const session = await getSession(context)
|
||||
let content = null
|
||||
|
||||
if (session) {
|
||||
const hostname = process.env.NEXTAUTH_URL || 'http://localhost:3000'
|
||||
const options = { headers: { cookie: context.req.headers.cookie } }
|
||||
const res = await fetch(`${hostname}/api/examples/protected`, options)
|
||||
const json = await res.json()
|
||||
if (json.content) { content = json.content }
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
session,
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
33
app/pages/protected.js
Normal file
33
app/pages/protected.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useSession } from 'next-auth/client'
|
||||
import Layout from '../components/layout'
|
||||
import AccessDenied from '../components/access-denied'
|
||||
|
||||
export default function Page () {
|
||||
const [session, loading] = useSession()
|
||||
const [content, setContent] = useState()
|
||||
|
||||
// Fetch content from protected route
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const res = await fetch('/api/examples/protected')
|
||||
const json = await res.json()
|
||||
if (json.content) { setContent(json.content) }
|
||||
}
|
||||
fetchData()
|
||||
}, [session])
|
||||
|
||||
// When rendering client side don't display anything until loading is complete
|
||||
if (typeof window !== 'undefined' && loading) return null
|
||||
|
||||
// If no session exists, display access denied message
|
||||
if (!session) { return <Layout><AccessDenied /></Layout> }
|
||||
|
||||
// If session exists, display content
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Protected Page</h1>
|
||||
<p><strong>{content}</strong></p>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
37
app/pages/server.js
Normal file
37
app/pages/server.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { getSession } from 'next-auth/client'
|
||||
import Layout from '../components/layout'
|
||||
|
||||
export default function Page () {
|
||||
// As this page uses Server Side Rendering, the `session` will be already
|
||||
// populated on render without needing to go through a loading stage.
|
||||
// This is possible because of the shared context configured in `_app.js` that
|
||||
// is used by `useSession()`.
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Server Side Rendering</h1>
|
||||
<p>
|
||||
This page uses the universal <strong>getSession()</strong> method in <strong>getServerSideProps()</strong>.
|
||||
</p>
|
||||
<p>
|
||||
Using <strong>getSession()</strong> in <strong>getServerSideProps()</strong> is the recommended approach if you need to
|
||||
support Server Side Rendering with authentication.
|
||||
</p>
|
||||
<p>
|
||||
The advantage of Server Side Rendering is this page does not require client side JavaScript.
|
||||
</p>
|
||||
<p>
|
||||
The disadvantage of Server Side Rendering is that this page is slower to render.
|
||||
</p>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
// Export the `session` prop to use sessions with Server Side Rendering
|
||||
export async function getServerSideProps (context) {
|
||||
return {
|
||||
props: {
|
||||
session: await getSession(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
30
app/pages/styles.css
Normal file
30
app/pages/styles.css
Normal file
@@ -0,0 +1,30 @@
|
||||
body {
|
||||
font-family: -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
padding: 0 1rem 1rem 1rem;
|
||||
max-width: 680px;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
li,
|
||||
p {
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
iframe {
|
||||
background: #ccc;
|
||||
border: 1px solid #ccc;
|
||||
height: 10rem;
|
||||
width: 100%;
|
||||
border-radius: .5rem;
|
||||
filter: invert(1);
|
||||
}
|
||||
63
app/prisma/schema.prisma
Normal file
63
app/prisma/schema.prisma
Normal file
@@ -0,0 +1,63 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Account {
|
||||
id Int @default(autoincrement()) @id
|
||||
compoundId String @unique @map(name: "compound_id")
|
||||
userId Int @map(name: "user_id")
|
||||
providerType String @map(name: "provider_type")
|
||||
providerId String @map(name: "provider_id")
|
||||
providerAccountId String @map(name: "provider_account_id")
|
||||
refreshToken String? @map(name: "refresh_token")
|
||||
accessToken String? @map(name: "access_token")
|
||||
accessTokenExpires DateTime? @map(name: "access_token_expires")
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
|
||||
@@index([providerAccountId], name: "providerAccountId")
|
||||
@@index([providerId], name: "providerId")
|
||||
@@index([userId], name: "userId")
|
||||
|
||||
@@map(name: "accounts")
|
||||
}
|
||||
|
||||
model Session {
|
||||
id Int @default(autoincrement()) @id
|
||||
userId Int @map(name: "user_id")
|
||||
expires DateTime
|
||||
sessionToken String @unique @map(name: "session_token")
|
||||
accessToken String @unique @map(name: "access_token")
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
|
||||
@@map(name: "sessions")
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @default(autoincrement()) @id
|
||||
name String?
|
||||
email String? @unique
|
||||
emailVerified DateTime? @map(name: "email_verified")
|
||||
image String?
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
|
||||
@@map(name: "users")
|
||||
}
|
||||
|
||||
model VerificationRequest {
|
||||
id Int @default(autoincrement()) @id
|
||||
identifier String
|
||||
token String @unique
|
||||
expires DateTime
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
|
||||
@@map(name: "verification_requests")
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
["@babel/preset-env", { "targets": { "esmodules": true } } ]
|
||||
],
|
||||
"comments": false,
|
||||
"overrides": [
|
||||
{
|
||||
"test": [ "./src/server/pages/**" ],
|
||||
"presets": [ "preact" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
15
config/babel.config.json
Normal file
15
config/babel.config.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"presets": [
|
||||
["@babel/preset-env", { "targets": { "esmodules": true } }]
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-class-properties"
|
||||
],
|
||||
"comments": false,
|
||||
"overrides": [
|
||||
{
|
||||
"test": ["../src/server/pages/**"],
|
||||
"presets": ["preact"]
|
||||
}
|
||||
]
|
||||
}
|
||||
91
config/build.js
Normal file
91
config/build.js
Normal file
@@ -0,0 +1,91 @@
|
||||
const fs = require("fs-extra")
|
||||
const path = require("path")
|
||||
|
||||
const MODULE_ENTRIES = {
|
||||
SERVER: "index",
|
||||
CLIENT: "client",
|
||||
PROVIDERS: "providers",
|
||||
ADAPTERS: "adapters",
|
||||
JWT: "jwt",
|
||||
ERRORS: "errors",
|
||||
}
|
||||
|
||||
// Building submodule entries
|
||||
|
||||
const BUILD_TARGETS = {
|
||||
[`${MODULE_ENTRIES.SERVER}.js`]: "module.exports = require('./dist/server').default\n",
|
||||
[`${MODULE_ENTRIES.CLIENT}.js`]: "module.exports = require('./dist/client').default\n",
|
||||
[`${MODULE_ENTRIES.ADAPTERS}.js`]: "module.exports = require('./dist/adapters').default\n",
|
||||
[`${MODULE_ENTRIES.PROVIDERS}.js`]: "module.exports = require('./dist/providers').default\n",
|
||||
[`${MODULE_ENTRIES.JWT}.js`]: "module.exports = require('./dist/lib/jwt').default\n",
|
||||
[`${MODULE_ENTRIES.ERRORS}.js`]: "module.exports = require('./dist/lib/errors').default\n",
|
||||
}
|
||||
|
||||
Object.entries(BUILD_TARGETS).forEach(([target, content]) => {
|
||||
fs.writeFile(path.join(process.cwd(), target), content, (err) => {
|
||||
if (err) throw err
|
||||
console.log(`[build] created "${target}" in root folder`)
|
||||
})
|
||||
})
|
||||
|
||||
// Building types
|
||||
|
||||
const TYPES_TARGETS = [
|
||||
`${MODULE_ENTRIES.SERVER}.d.ts`,
|
||||
`${MODULE_ENTRIES.CLIENT}.d.ts`,
|
||||
`${MODULE_ENTRIES.ADAPTERS}.d.ts`,
|
||||
`${MODULE_ENTRIES.PROVIDERS}.d.ts`,
|
||||
`${MODULE_ENTRIES.JWT}.d.ts`,
|
||||
`${MODULE_ENTRIES.ERRORS}.d.ts`,
|
||||
"internals",
|
||||
]
|
||||
|
||||
TYPES_TARGETS.forEach((target) => {
|
||||
fs.copy(
|
||||
path.resolve("types", target),
|
||||
path.join(process.cwd(), target),
|
||||
(err) => {
|
||||
if (err) throw err
|
||||
console.log(`[build-types] copying "${target}" to root folder`)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
// Building providers
|
||||
|
||||
const providersDir = path.join(process.cwd(), "/src/providers")
|
||||
|
||||
const files = fs
|
||||
.readdirSync(providersDir, "utf8")
|
||||
.filter((file) => file !== "index.js")
|
||||
|
||||
let importLines = ""
|
||||
let exportLines = `export default {\n`
|
||||
files.forEach((file) => {
|
||||
const provider = fs.readFileSync(path.join(providersDir, file), "utf8")
|
||||
try {
|
||||
// NOTE: If this fails, the default export probably wasn't a named function.
|
||||
// Always use a named function as default export.
|
||||
// Eg.: export default function YourProvider ...
|
||||
const { functionName } = provider.match(
|
||||
/export default function (?<functionName>.+)\s?\(/
|
||||
).groups
|
||||
|
||||
importLines += `import ${functionName} from "./${file}"\n`
|
||||
exportLines += ` ${functionName},\n`
|
||||
} catch (error) {
|
||||
console.error(
|
||||
[
|
||||
`\nThe provider file '${file}' should have a single named default export`,
|
||||
"Example: 'export default function YourProvider'\n\n",
|
||||
].join("\n")
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
exportLines += `}\n`
|
||||
|
||||
fs.writeFile(
|
||||
path.join(process.cwd(), "src/providers/index.js"),
|
||||
[importLines, exportLines].join("\n")
|
||||
)
|
||||
@@ -1,6 +1,6 @@
|
||||
// Serverless target in Next.js does not work if you try to read in files at runtime
|
||||
// that are not JavaScript or JSON (e.g. CSS files).
|
||||
// https://github.com/iaincollins/next-auth/issues/281
|
||||
// https://github.com/nextauthjs/next-auth/issues/281
|
||||
//
|
||||
// To work around this issue, this script is a manual step that wraps CSS in a
|
||||
// JavaScript file that has the compiled CSS embedded in it, and exports only
|
||||
15327
package-lock.json
generated
15327
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
176
package.json
176
package.json
@@ -1,44 +1,76 @@
|
||||
{
|
||||
"name": "next-auth",
|
||||
"version": "3.0.0",
|
||||
"version": "0.0.0-semantically-released",
|
||||
"description": "Authentication for Next.js",
|
||||
"homepage": "https://next-auth.js.org",
|
||||
"repository": "https://github.com/nextauthjs/next-auth.git",
|
||||
"author": "Iain Collins <me@iaincollins.com>",
|
||||
"main": "index.js",
|
||||
"types": "./index.d.ts",
|
||||
"keywords": [
|
||||
"react",
|
||||
"nodejs",
|
||||
"oauth",
|
||||
"jwt",
|
||||
"oauth2",
|
||||
"authentication",
|
||||
"nextjs",
|
||||
"csrf",
|
||||
"oidc",
|
||||
"nextauth"
|
||||
],
|
||||
"exports": {
|
||||
".": "./dist/server/index.js",
|
||||
"./jwt": "./dist/lib/jwt.js",
|
||||
"./adapters": "./dist/adapters/index.js",
|
||||
"./client": "./dist/client/index.js",
|
||||
"./providers": "./dist/providers/index.js",
|
||||
"./providers/*": "./dist/providers/*.js",
|
||||
"./errors": "./dist/lib/errors.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run build:js && npm run build:css",
|
||||
"build:js": "babel src --out-dir dist",
|
||||
"build:css": "postcss src/**/*.css --base src --dir dist && node scripts/wrap-css.js",
|
||||
"build:js": "node ./config/build.js && babel --config-file ./config/babel.config.json src --out-dir dist",
|
||||
"build:css": "postcss --config config/postcss.config.js src/**/*.css --base src --dir dist && node config/wrap-css.js",
|
||||
"dev:setup": "npm run build:css && cd app && npm i",
|
||||
"dev": "cd app && npm run dev",
|
||||
"watch": "npm run watch:js | npm run watch:css",
|
||||
"watch:js": "babel --watch src --out-dir dist",
|
||||
"watch:css": "postcss --watch src/**/*.css --base src --dir dist",
|
||||
"test": "npm run lint",
|
||||
"test:db": "npm run test:db:mysql && npm run test:db:postgres && npm run test:db:mongodb",
|
||||
"watch:js": "babel --config-file ./config/babel.config.json --watch src --out-dir dist",
|
||||
"watch:css": "postcss --config config/postcss.config.js --watch src/**/*.css --base src --dir dist",
|
||||
"test:app:start": "docker-compose -f test/docker/app.yml up -d",
|
||||
"test:app:rebuild": "npm run build && docker-compose -f test/docker/app.yml up -d --build",
|
||||
"test:app:stop": "docker-compose -f test/docker/app.yml down",
|
||||
"test": "npm run test:app:rebuild && npm run test:integration && npm run test:app:stop && npm run test:types",
|
||||
"test:db": "npm run test:db:mysql && npm run test:db:postgres && npm run test:db:mongodb && npm run test:db:mssql",
|
||||
"test:db:mysql": "node test/mysql.js",
|
||||
"test:db:postgres": "node test/postgres.js",
|
||||
"test:db:mongodb": "node test/mongodb.js",
|
||||
"db:start": "docker-compose -f test/docker/docker-compose.yml up -d",
|
||||
"db:start:mongo": "docker-compose -f test/docker/mongo.yml up -d",
|
||||
"db:start:mysql": "docker-compose -f test/docker/mysql.yml up -d",
|
||||
"db:start:postgres": "docker-compose -f test/docker/postgres.yml up -d",
|
||||
"db:stop": "docker-compose -f test/docker/docker-compose.yml down",
|
||||
"db:stop:mongo": "docker-compose -f test/docker/mongo.yml down",
|
||||
"db:stop:mysql": "docker-compose -f test/docker/mysql.yml down",
|
||||
"db:stop:postgres": "docker-compose -f test/docker/postgres.yml down",
|
||||
"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",
|
||||
"prepublishOnly": "npm run build",
|
||||
"publish:beta": "npm publish --tag beta",
|
||||
"publish:canary": "npm publish --tag canary",
|
||||
"lint": "standard",
|
||||
"lint:fix": "standard --fix"
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"index.js",
|
||||
"index.d.ts",
|
||||
"providers.js",
|
||||
"providers.d.ts",
|
||||
"adapters.js",
|
||||
"adapters.d.ts",
|
||||
"client.js",
|
||||
"jwt.js"
|
||||
"client.d.ts",
|
||||
"errors.js",
|
||||
"errors.d.ts",
|
||||
"jwt.js",
|
||||
"jwt.d.ts",
|
||||
"internals"
|
||||
],
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -46,38 +78,126 @@
|
||||
"futoin-hkdf": "^1.3.2",
|
||||
"jose": "^1.27.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jwt-decode": "^2.2.0",
|
||||
"node-jose": "^1.1.4",
|
||||
"nodemailer": "^6.4.6",
|
||||
"nodemailer": "^6.4.16",
|
||||
"oauth": "^0.9.15",
|
||||
"pkce-challenge": "^2.1.0",
|
||||
"preact": "^10.4.1",
|
||||
"preact-render-to-string": "^5.1.7",
|
||||
"preact-render-to-string": "^5.1.14",
|
||||
"querystring": "^0.2.0",
|
||||
"require_optional": "^1.0.1",
|
||||
"typeorm": "^0.2.24"
|
||||
"typeorm": "^0.2.30"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1"
|
||||
"react": "^16.13.1 || ^17",
|
||||
"react-dom": "16.13.1 || ^17"
|
||||
},
|
||||
"peerOptionalDependencies": {
|
||||
"mongodb": "^3.5.9",
|
||||
"mysql": "^2.18.1",
|
||||
"mssql": "^6.2.1",
|
||||
"pg": "^8.2.1",
|
||||
"@prisma/client": "^2.3.0"
|
||||
"@prisma/client": "^2.16.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.8.4",
|
||||
"@babel/core": "^7.9.6",
|
||||
"@babel/plugin-proposal-class-properties": "^7.13.0",
|
||||
"@babel/preset-env": "^7.9.6",
|
||||
"@prisma/client": "^2.16.1",
|
||||
"@semantic-release/commit-analyzer": "^8.0.1",
|
||||
"@semantic-release/github": "^7.2.0",
|
||||
"@semantic-release/npm": "7.0.8",
|
||||
"@semantic-release/release-notes-generator": "^9.0.1",
|
||||
"@types/react": "^17.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.22.0",
|
||||
"@typescript-eslint/parser": "^4.22.0",
|
||||
"autoprefixer": "^9.7.6",
|
||||
"babel-preset-preact": "^2.0.0",
|
||||
"conventional-changelog-conventionalcommits": "4.4.0",
|
||||
"cssnano": "^4.1.10",
|
||||
"dotenv": "^8.2.0",
|
||||
"dtslint": "^4.0.8",
|
||||
"eslint": "^7.19.0",
|
||||
"eslint-config-prettier": "^8.2.0",
|
||||
"eslint-config-standard-with-typescript": "^19.0.1",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"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",
|
||||
"postcss-cli": "^7.1.1",
|
||||
"postcss-nested": "^4.2.1",
|
||||
"standard": "^14.3.3"
|
||||
}
|
||||
"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"
|
||||
},
|
||||
"prettier": {
|
||||
"semi": false
|
||||
},
|
||||
"eslintConfig": {
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"extends": [
|
||||
"standard-with-typescript",
|
||||
"prettier"
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"node_modules",
|
||||
"test",
|
||||
"next-env.d.ts",
|
||||
"types",
|
||||
"www",
|
||||
".next",
|
||||
"dist"
|
||||
],
|
||||
"globals": {
|
||||
"localStorage": "readonly",
|
||||
"location": "readonly",
|
||||
"fetch": "readonly"
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"branches": [
|
||||
"+([0-9])?(.{+([0-9]),x}).x",
|
||||
"main",
|
||||
{
|
||||
"name": "beta",
|
||||
"prerelease": true
|
||||
},
|
||||
{
|
||||
"name": "next",
|
||||
"prerelease": true
|
||||
}
|
||||
],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
"@semantic-release/release-notes-generator",
|
||||
"@semantic-release/npm",
|
||||
[
|
||||
"@semantic-release/github",
|
||||
{
|
||||
"releasedLabels": false,
|
||||
"successComment": false
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/balazsorban44"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
module.exports = require('./dist/providers').default
|
||||
@@ -1,84 +1,83 @@
|
||||
const Adapter = (config, options = {}) => {
|
||||
async function getAdapter (appOptions) {
|
||||
const { logger } = appOptions
|
||||
// Display debug output if debug option enabled
|
||||
function _debug (...args) {
|
||||
if (appOptions.debug) {
|
||||
console.log('[next-auth][debug]', ...args)
|
||||
}
|
||||
function debug (debugCode, ...args) {
|
||||
logger.debug(`ADAPTER_${debugCode}`, ...args)
|
||||
}
|
||||
|
||||
async function createUser (profile) {
|
||||
_debug('createUser', profile)
|
||||
debug('createUser', profile)
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUser (id) {
|
||||
_debug('getUser', id)
|
||||
debug('getUser', id)
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUserByEmail (email) {
|
||||
_debug('getUserByEmail', email)
|
||||
debug('getUserByEmail', email)
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUserByProviderAccountId (providerId, providerAccountId) {
|
||||
_debug('getUserByProviderAccountId', providerId, providerAccountId)
|
||||
debug('getUserByProviderAccountId', providerId, providerAccountId)
|
||||
return null
|
||||
}
|
||||
|
||||
async function updateUser (user) {
|
||||
_debug('updateUser', user)
|
||||
debug('updateUser', user)
|
||||
return null
|
||||
}
|
||||
|
||||
async function deleteUser (userId) {
|
||||
_debug('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)
|
||||
debug('linkAccount', userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires)
|
||||
return null
|
||||
}
|
||||
|
||||
async function unlinkAccount (userId, providerId, providerAccountId) {
|
||||
_debug('unlinkAccount', userId, providerId, providerAccountId)
|
||||
debug('unlinkAccount', userId, providerId, providerAccountId)
|
||||
return null
|
||||
}
|
||||
|
||||
async function createSession (user) {
|
||||
_debug('createSession', user)
|
||||
debug('createSession', user)
|
||||
return null
|
||||
}
|
||||
|
||||
async function getSession (sessionToken) {
|
||||
_debug('getSession', sessionToken)
|
||||
debug('getSession', sessionToken)
|
||||
return null
|
||||
}
|
||||
|
||||
async function updateSession (session, force) {
|
||||
_debug('updateSession', session)
|
||||
debug('updateSession', session)
|
||||
return null
|
||||
}
|
||||
|
||||
async function deleteSession (sessionToken) {
|
||||
_debug('deleteSession', sessionToken)
|
||||
debug('deleteSession', sessionToken)
|
||||
return null
|
||||
}
|
||||
|
||||
async function createVerificationRequest (identifier, url, token, secret, provider) {
|
||||
_debug('createVerificationRequest', identifier)
|
||||
debug('createVerificationRequest', identifier)
|
||||
return null
|
||||
}
|
||||
|
||||
async function getVerificationRequest (identifier, token, secret, provider) {
|
||||
_debug('getVerificationRequest', identifier, token)
|
||||
debug('getVerificationRequest', identifier, token)
|
||||
return null
|
||||
}
|
||||
|
||||
async function deleteVerificationRequest (identifier, token, secret, provider) {
|
||||
_debug('deleteVerification', identifier, token)
|
||||
debug('deleteVerification', identifier, token)
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { createHash, randomBytes } from 'crypto'
|
||||
|
||||
import { CreateUserError } from '../../lib/errors'
|
||||
import logger from '../../lib/logger'
|
||||
|
||||
const Adapter = (config) => {
|
||||
const {
|
||||
@@ -21,6 +20,7 @@ const Adapter = (config) => {
|
||||
}
|
||||
|
||||
async function getAdapter (appOptions) {
|
||||
const { logger } = appOptions
|
||||
function debug (debugCode, ...args) {
|
||||
logger.debug(`PRISMA_${debugCode}`, ...args)
|
||||
}
|
||||
@@ -57,7 +57,7 @@ const Adapter = (config) => {
|
||||
async function getUser (id) {
|
||||
debug('GET_USER', id)
|
||||
try {
|
||||
return prisma[User].findOne({ where: { id } })
|
||||
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))
|
||||
@@ -68,7 +68,7 @@ const Adapter = (config) => {
|
||||
debug('GET_USER_BY_EMAIL', email)
|
||||
try {
|
||||
if (!email) { return Promise.resolve(null) }
|
||||
return prisma[User].findOne({ where: { email } })
|
||||
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))
|
||||
@@ -78,7 +78,9 @@ const Adapter = (config) => {
|
||||
async function getUserByProviderAccountId (providerId, providerAccountId) {
|
||||
debug('GET_USER_BY_PROVIDER_ACCOUNT_ID', providerId, providerAccountId)
|
||||
try {
|
||||
return prisma[Account].findOne({ where: { compoundId: getCompoundId(providerId, providerAccountId) } })
|
||||
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))
|
||||
@@ -172,7 +174,7 @@ const Adapter = (config) => {
|
||||
async function getSession (sessionToken) {
|
||||
debug('GET_SESSION', sessionToken)
|
||||
try {
|
||||
const session = await prisma[Session].findOne({ where: { sessionToken } })
|
||||
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) {
|
||||
@@ -217,7 +219,7 @@ const Adapter = (config) => {
|
||||
}
|
||||
|
||||
const { id, expires } = session
|
||||
return prisma[Session].update({ where: { id }, data: { expires } })
|
||||
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))
|
||||
@@ -278,11 +280,15 @@ const Adapter = (config) => {
|
||||
// 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].findOne({ where: { token: hashedToken } })
|
||||
|
||||
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].delete({ where: { token: hashedToken } })
|
||||
await prisma[VerificationRequest].deleteMany({ where: { identifier, token: hashedToken } })
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -298,7 +304,7 @@ const Adapter = (config) => {
|
||||
try {
|
||||
// Delete verification entry so it cannot be used again
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
await prisma[VerificationRequest].delete({ where: { token: hashedToken } })
|
||||
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))
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { createConnection, getConnection } from 'typeorm'
|
||||
import { createHash } from 'crypto'
|
||||
import require_optional from 'require_optional'
|
||||
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 logger from '../../lib/logger'
|
||||
|
||||
import { updateConnectionEntities } from './lib/utils'
|
||||
|
||||
const Adapter = (typeOrmConfig, options = {}) => {
|
||||
// Ensure typeOrmConfigObject is normalized to an object
|
||||
@@ -40,6 +41,12 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
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 () {
|
||||
@@ -68,16 +75,14 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
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
|
||||
|
||||
// Display debug output if debug option enabled
|
||||
// @TODO Refactor logger so is passed in appOptions
|
||||
function debug (debugCode, ...args) {
|
||||
logger.debug(`TYPEORM_${debugCode}`, ...args)
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -171,7 +176,7 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
|
||||
async function updateUser (user) {
|
||||
debug('UPDATE_USER', user)
|
||||
return manager.save(user)
|
||||
return manager.save(User, user)
|
||||
}
|
||||
|
||||
async function deleteUser (userId) {
|
||||
@@ -266,7 +271,7 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
if (!force) { return null }
|
||||
}
|
||||
|
||||
return manager.save(session)
|
||||
return manager.save(Session, session)
|
||||
} catch (error) {
|
||||
logger.error('UPDATE_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('UPDATE_SESSION_ERROR', error))
|
||||
@@ -326,7 +331,7 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
|
||||
if (verificationRequest && verificationRequest.expires && new Date() > new Date(verificationRequest.expires)) {
|
||||
// Delete verification entry so it cannot be used again
|
||||
await manager.delete(VerificationRequest, { token: hashedToken })
|
||||
await manager.delete(VerificationRequest, { identifier, token: hashedToken })
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -342,7 +347,7 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
try {
|
||||
// Delete verification entry so it cannot be used again
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
await manager.delete(VerificationRequest, { token: hashedToken })
|
||||
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))
|
||||
|
||||
@@ -25,6 +25,7 @@ const parseConnectionString = (configString) => {
|
||||
config.username = parsedUrl.username
|
||||
config.password = parsedUrl.password
|
||||
config.database = parsedUrl.pathname.replace(/^\//, '').replace(/\?(.*)$/, '')
|
||||
config.options = {}
|
||||
}
|
||||
|
||||
// This option is recommended by mongodb
|
||||
@@ -32,6 +33,11 @@ const parseConnectionString = (configString) => {
|
||||
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('=')
|
||||
|
||||
@@ -74,14 +74,15 @@ const mongodbTransform = (models, options) => {
|
||||
// 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
|
||||
models.User.schema.indices = [
|
||||
{
|
||||
name: 'email',
|
||||
unique: true,
|
||||
sparse: true,
|
||||
columns: ['email']
|
||||
}
|
||||
]
|
||||
|
||||
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) => {
|
||||
@@ -107,6 +108,37 @@ const sqliteTransform = (models, options) => {
|
||||
}
|
||||
}
|
||||
|
||||
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')) ||
|
||||
@@ -121,6 +153,9 @@ export default (config, 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.
|
||||
|
||||
18
src/adapters/typeorm/lib/utils.js
Normal file
18
src/adapters/typeorm/lib/utils.js
Normal file
@@ -0,0 +1,18 @@
|
||||
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,5 +1,3 @@
|
||||
/// Note: fetch() is built in to Next.js 9.4
|
||||
//
|
||||
// Note about signIn() and signOut() methods:
|
||||
//
|
||||
// On signIn() and signOut() we pass 'json: true' to request a response in JSON
|
||||
@@ -10,9 +8,8 @@
|
||||
//
|
||||
// We use HTTP POST requests with CSRF Tokens to protect against CSRF attacks.
|
||||
|
||||
/* global fetch:false */
|
||||
import { useState, useEffect, useContext, createContext, createElement } from 'react'
|
||||
import logger from '../lib/logger'
|
||||
import _logger, { proxyLogger } from '../lib/logger'
|
||||
import parseUrl from '../lib/parse-url'
|
||||
|
||||
// This behaviour mirrors the default behaviour for getting the site name that
|
||||
@@ -21,302 +18,340 @@ import parseUrl from '../lib/parse-url'
|
||||
// relative URLs are valid in that context and so defaults to empty.
|
||||
// 2. When invoked server side the value is picked up from an environment
|
||||
// variable and defaults to 'http://localhost:3000'.
|
||||
/** @type {import("types/internals/client").NextAuthConfig} */
|
||||
const __NEXTAUTH = {
|
||||
baseUrl: parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL).baseUrl,
|
||||
basePath: parseUrl(process.env.NEXTAUTH_URL).basePath,
|
||||
keepAlive: 0, // 0 == disabled (don't send); 60 == send every 60 seconds
|
||||
clientMaxAge: 0, // 0 == disabled (only use cache); 60 == sync if last checked > 60 seconds ago
|
||||
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
|
||||
_clientLastSync: 0, // used for timestamp since last sycned (in seconds)
|
||||
_clientSyncTimer: null, // stores timer for poll interval
|
||||
_eventListenersAdded: false, // tracks if event listeners have been added,
|
||||
_clientSession: undefined, // stores last session response from hook,
|
||||
// Generate a unique ID to make it possible to identify when a message
|
||||
// was sent from this tab/window so it can be ignored to avoid event loops.
|
||||
_clientId: Math.random().toString(36).substring(2) + Date.now().toString(36),
|
||||
// Used to store to function export by getSession() hook
|
||||
_clientLastSync: 0,
|
||||
_clientSyncTimer: null,
|
||||
_eventListenersAdded: false,
|
||||
_clientSession: undefined,
|
||||
_getSession: () => {}
|
||||
}
|
||||
|
||||
// Add event listners on load
|
||||
if (typeof window !== 'undefined') {
|
||||
if (__NEXTAUTH._eventListenersAdded === false) {
|
||||
__NEXTAUTH._eventListenersAdded = true
|
||||
const logger = proxyLogger(_logger, __NEXTAUTH.basePath)
|
||||
|
||||
// Listen for storage events and update session if event fired from
|
||||
// another window (but suppress firing another event to avoid a loop)
|
||||
window.addEventListener('storage', async (event) => {
|
||||
if (event.key === 'nextauth.message') {
|
||||
const message = JSON.parse(event.newValue)
|
||||
if (message.event && message.event === 'session' && message.data) {
|
||||
// Ignore storage events fired from the same window that created them
|
||||
if (__NEXTAUTH._clientId === message.clientId) {
|
||||
const broadcast = BroadcastChannel()
|
||||
|
||||
// Add event listners on load
|
||||
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)
|
||||
// Fetch new session data but tell it to not to fire another event to
|
||||
// avoid an infinite loop.
|
||||
// Note: We could pass session data through and do something like
|
||||
// `setData(message.data)` but that can cause problems depending
|
||||
// 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' }))
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Context to store session data globally
|
||||
/** @type {import("types/internals/client").SessionContext} */
|
||||
const SessionContext = createContext()
|
||||
|
||||
export function useSession (session) {
|
||||
const context = useContext(SessionContext)
|
||||
if (context) return context
|
||||
return _useSessionHook(session)
|
||||
}
|
||||
|
||||
function _useSessionHook (session) {
|
||||
const [data, setData] = useState(session)
|
||||
const [loading, setLoading] = useState(!data)
|
||||
|
||||
useEffect(() => {
|
||||
__NEXTAUTH._getSession = async ({ event = null } = {}) => {
|
||||
try {
|
||||
const triggredByEvent = event !== null
|
||||
const triggeredByStorageEvent = event === 'storage'
|
||||
|
||||
const clientMaxAge = __NEXTAUTH.clientMaxAge
|
||||
const clientLastSync = parseInt(__NEXTAUTH._clientLastSync)
|
||||
const currentTime = _now()
|
||||
const clientSession = __NEXTAUTH._clientSession
|
||||
|
||||
// Updates triggered by a storage event *always* trigger an update and we
|
||||
// always update if we don't have any value for the current session state.
|
||||
if (!triggeredByStorageEvent && clientSession !== undefined) {
|
||||
if (clientMaxAge === 0 && triggredByEvent !== true) {
|
||||
// If there is no time defined for when a session should be considered
|
||||
// stale, then it's okay to use the value we have until an event is
|
||||
// triggered which updates it.
|
||||
return
|
||||
} else if (clientMaxAge > 0 && clientSession === null) {
|
||||
// If the client doesn't have a session then we don't need to call
|
||||
// the server to check if it does (if they have signed in via another
|
||||
// tab or window that will come through as a triggeredByStorageEvent
|
||||
// event and will skip this logic)
|
||||
return
|
||||
} 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
|
||||
}
|
||||
|
||||
// Fetch new session data but pass 'true' to it not to fire an event to
|
||||
// avoid an infinite loop.
|
||||
//
|
||||
// Note: We could pass session data through and do something like
|
||||
// `setData(message.data)` but that can cause problems depending
|
||||
// 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.
|
||||
await __NEXTAUTH._getSession({ event: 'storage' })
|
||||
}
|
||||
|
||||
if (clientSession === undefined) { __NEXTAUTH._clientSession = null }
|
||||
|
||||
// Update clientLastSync before making response to avoid repeated
|
||||
// invokations that would otherwise be triggered while we are still
|
||||
// waiting for a response.
|
||||
__NEXTAUTH._clientLastSync = _now()
|
||||
|
||||
// If this call was invoked via a storage event (i.e. another window) then
|
||||
// tell getSession not to trigger an event when it calls to avoid an
|
||||
// infinate loop.
|
||||
const newClientSessionData = await getSession({
|
||||
triggerEvent: !triggeredByStorageEvent
|
||||
})
|
||||
|
||||
// Save session state internally, just so we can track that we've checked
|
||||
// if a session exists at least once.
|
||||
__NEXTAUTH._clientSession = newClientSessionData
|
||||
|
||||
setData(newClientSessionData)
|
||||
setLoading(false)
|
||||
} catch (error) {
|
||||
logger.error('CLIENT_USE_SESSION_ERROR', error)
|
||||
setLoading(false)
|
||||
}
|
||||
})
|
||||
|
||||
// Listen for window focus/blur events
|
||||
window.addEventListener('focus', async (event) => __NEXTAUTH._getSession({ event: 'focus' }))
|
||||
window.addEventListener('blur', async (event) => __NEXTAUTH._getSession({ event: 'blur' }))
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
const 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' && keepAlive > 0) {
|
||||
// Clear existing timer (if there is one)
|
||||
if (__NEXTAUTH._clientSyncTimer !== null) { clearTimeout(__NEXTAUTH._clientSyncTimer) }
|
||||
|
||||
// Set next timer to trigger in number of seconds
|
||||
__NEXTAUTH._clientSyncTimer = setTimeout(async () => {
|
||||
// Only invoke keepalive when a session exists
|
||||
if (__NEXTAUTH._clientSession) {
|
||||
await __NEXTAUTH._getSession({ event: 'timer' })
|
||||
}
|
||||
}, keepAlive * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
__NEXTAUTH._getSession()
|
||||
})
|
||||
|
||||
return [data, loading]
|
||||
}
|
||||
|
||||
// Universal method (client + server)
|
||||
const getSession = async ({ req, ctx, triggerEvent = true } = {}) => {
|
||||
// If passed 'appContext' via getInitialProps() in _app.js then get the req
|
||||
// object from ctx and use that for the req value to allow getSession() to
|
||||
// work seemlessly in getInitialProps() on server side pages *and* in _app.js.
|
||||
if (!req && ctx && ctx.req) { req = ctx.req }
|
||||
|
||||
const baseUrl = _apiBaseUrl()
|
||||
const fetchOptions = req ? { headers: { cookie: req.headers.cookie } } : {}
|
||||
const session = await _fetchData(`${baseUrl}/session`, fetchOptions)
|
||||
if (triggerEvent) {
|
||||
_sendMessage({ event: 'session', data: { trigger: 'getSession' } })
|
||||
export async function getSession (ctx) {
|
||||
const session = await _fetchData('session', ctx)
|
||||
if (ctx?.triggerEvent ?? true) {
|
||||
broadcast.post({ event: 'session', data: { trigger: 'getSession' } })
|
||||
}
|
||||
return session
|
||||
}
|
||||
|
||||
// Universal method (client + server)
|
||||
const getCsrfToken = async ({ req, ctx } = {}) => {
|
||||
// If passed 'appContext' via getInitialProps() in _app.js then get the req
|
||||
// object from ctx and use that for the req value to allow getCsrfToken() to
|
||||
// work seemlessly in getInitialProps() on server side pages *and* in _app.js.
|
||||
if (!req && ctx && ctx.req) { req = ctx.req }
|
||||
export async function getCsrfToken (ctx) {
|
||||
return (await _fetchData('csrf', ctx))?.csrfToken
|
||||
}
|
||||
|
||||
export async function getProviders () {
|
||||
return _fetchData('providers')
|
||||
}
|
||||
|
||||
export async function signIn (provider, options = {}, authorizationParams = {}) {
|
||||
const {
|
||||
callbackUrl = window.location,
|
||||
redirect = true
|
||||
} = options
|
||||
|
||||
const baseUrl = _apiBaseUrl()
|
||||
const fetchOptions = req ? { headers: { cookie: req.headers.cookie } } : {}
|
||||
const data = await _fetchData(`${baseUrl}/csrf`, fetchOptions)
|
||||
return data && data.csrfToken ? data.csrfToken : null
|
||||
}
|
||||
|
||||
// Universal method (client + server); does not require request headers
|
||||
const getProviders = async () => {
|
||||
const baseUrl = _apiBaseUrl()
|
||||
return _fetchData(`${baseUrl}/providers`)
|
||||
}
|
||||
|
||||
// Context to store session data globally
|
||||
const SessionContext = createContext()
|
||||
|
||||
// Client side method
|
||||
const useSession = (session) => {
|
||||
// Try to use context if we can
|
||||
const value = useContext(SessionContext)
|
||||
|
||||
// If we have no Provider in the tree, call the actual hook
|
||||
if (value === undefined) {
|
||||
return _useSessionHook(session)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
// Internal hook for getting session from the api.
|
||||
const _useSessionHook = (session) => {
|
||||
const [data, setData] = useState(session)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const _getSession = async ({ event = null } = {}) => {
|
||||
try {
|
||||
const triggredByEvent = (event !== null)
|
||||
const triggeredByStorageEvent = !!((event && event === 'storage'))
|
||||
|
||||
const clientMaxAge = __NEXTAUTH.clientMaxAge
|
||||
const clientLastSync = parseInt(__NEXTAUTH._clientLastSync)
|
||||
const currentTime = Math.floor(new Date().getTime() / 1000)
|
||||
const clientSession = __NEXTAUTH._clientSession
|
||||
|
||||
// Updates triggered by a storage event *always* trigger an update and we
|
||||
// always update if we don't have any value for the current session state.
|
||||
if (triggeredByStorageEvent === false && clientSession !== undefined) {
|
||||
if (clientMaxAge === 0 && triggredByEvent !== true) {
|
||||
// If there is no time defined for when a session should be considered
|
||||
// stale, then it's okay to use the value we have until an event is
|
||||
// triggered which updates it.
|
||||
return
|
||||
} else if (clientMaxAge > 0 && clientSession === null) {
|
||||
// If the client doesn't have a session then we don't need to call
|
||||
// the server to check if it does (if they have signed in via another
|
||||
// tab or window that will come through as a triggeredByStorageEvent
|
||||
// event and will skip this logic)
|
||||
return
|
||||
} 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 }
|
||||
|
||||
// Update clientLastSync before making response to avoid repeated
|
||||
// invokations that would otherwise be triggered while we are still
|
||||
// waiting for a response.
|
||||
__NEXTAUTH._clientLastSync = Math.floor(new Date().getTime() / 1000)
|
||||
|
||||
// If this call was invoked via a storage event (i.e. another window) then
|
||||
// tell getSession not to trigger an event when it calls to avoid an
|
||||
// infinate loop.
|
||||
const triggerEvent = (triggeredByStorageEvent === false)
|
||||
const newClientSessionData = await getSession({ triggerEvent })
|
||||
|
||||
// Save session state internally, just so we can track that we've checked
|
||||
// if a session exists at least once.
|
||||
__NEXTAUTH._clientSession = newClientSessionData
|
||||
|
||||
setData(newClientSessionData)
|
||||
setLoading(false)
|
||||
} catch (error) {
|
||||
logger.error('CLIENT_USE_SESSION_ERROR', error)
|
||||
}
|
||||
}
|
||||
|
||||
__NEXTAUTH._getSession = _getSession
|
||||
|
||||
useEffect(() => {
|
||||
_getSession()
|
||||
})
|
||||
return [data, loading]
|
||||
}
|
||||
|
||||
// Client side method
|
||||
const signIn = async (provider, args = {}) => {
|
||||
const baseUrl = _apiBaseUrl()
|
||||
const callbackUrl = (args && args.callbackUrl) ? args.callbackUrl : window.location
|
||||
const providers = await getProviders()
|
||||
|
||||
// Redirect to sign in page if no valid provider specified
|
||||
if (!provider || !providers[provider]) {
|
||||
if (!(provider in providers)) {
|
||||
// If Provider not recognized, redirect to sign in page
|
||||
window.location = `${baseUrl}/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
|
||||
} else {
|
||||
const signInUrl = (providers[provider].type === 'credentials')
|
||||
? `${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',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: _encodedForm({
|
||||
...args,
|
||||
csrfToken: await getCsrfToken(),
|
||||
callbackUrl: callbackUrl,
|
||||
json: true
|
||||
})
|
||||
}
|
||||
const res = await fetch(signInUrl, fetchOptions)
|
||||
const data = await res.json()
|
||||
window.location = data.url ? data.url : callbackUrl
|
||||
return
|
||||
}
|
||||
const isCredentials = providers[provider].type === 'credentials'
|
||||
const isEmail = providers[provider].type === 'email'
|
||||
const canRedirectBeDisabled = 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',
|
||||
headers: {
|
||||
'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()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const error = new URL(data.url).searchParams.get('error')
|
||||
|
||||
if (res.ok) {
|
||||
await __NEXTAUTH._getSession({ event: 'storage' })
|
||||
}
|
||||
|
||||
return {
|
||||
error,
|
||||
status: res.status,
|
||||
ok: res.ok,
|
||||
url: error ? null : data.url
|
||||
}
|
||||
}
|
||||
|
||||
// Client side method
|
||||
const signOut = async (args = {}) => {
|
||||
const callbackUrl = (args && args.callbackUrl) ? args.callbackUrl : window.location
|
||||
|
||||
export async function signOut (options = {}) {
|
||||
const {
|
||||
callbackUrl = window.location,
|
||||
redirect = true
|
||||
} = options
|
||||
const baseUrl = _apiBaseUrl()
|
||||
const fetchOptions = {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: _encodedForm({
|
||||
body: new URLSearchParams({
|
||||
csrfToken: await getCsrfToken(),
|
||||
callbackUrl: callbackUrl,
|
||||
callbackUrl,
|
||||
json: true
|
||||
})
|
||||
}
|
||||
const res = await fetch(`${baseUrl}/signout`, fetchOptions)
|
||||
const data = await res.json()
|
||||
_sendMessage({ event: 'session', data: { trigger: 'signout' } })
|
||||
window.location = data.url ? data.url : callbackUrl
|
||||
broadcast.post({ event: 'session', data: { trigger: 'signout' } })
|
||||
if (redirect) {
|
||||
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()
|
||||
return
|
||||
}
|
||||
|
||||
await __NEXTAUTH._getSession({ event: 'storage' })
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// Provider to wrap the app in to make session data available globally
|
||||
const Provider = ({ children, session, options }) => {
|
||||
setOptions(options)
|
||||
return createElement(SessionContext.Provider, { value: useSession(session) }, children)
|
||||
}
|
||||
// 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 } = {}) {
|
||||
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
|
||||
|
||||
const _fetchData = async (url, options = {}) => {
|
||||
try {
|
||||
const res = await fetch(url, options)
|
||||
const data = await res.json()
|
||||
return Promise.resolve(Object.keys(data).length > 0 ? data : null) // Return null if data empty
|
||||
} catch (error) {
|
||||
logger.error('CLIENT_FETCH_ERROR', url, error)
|
||||
return Promise.resolve(null)
|
||||
// Clear existing timer (if there is one)
|
||||
if (__NEXTAUTH._clientSyncTimer !== null) {
|
||||
clearTimeout(__NEXTAUTH._clientSyncTimer)
|
||||
}
|
||||
|
||||
// Set next timer to trigger in number of seconds
|
||||
__NEXTAUTH._clientSyncTimer = setTimeout(async () => {
|
||||
// Only invoke keepalive when a session exists
|
||||
if (!__NEXTAUTH._clientSession) return
|
||||
await __NEXTAUTH._getSession({ event: 'timer' })
|
||||
}, keepAlive * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
const _apiBaseUrl = () => {
|
||||
export function Provider ({ children, session, options }) {
|
||||
setOptions(options)
|
||||
return createElement(
|
||||
SessionContext.Provider,
|
||||
{ value: useSession(session) },
|
||||
children
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* If passed 'appContext' via getInitialProps() in _app.js
|
||||
* then get the req object from ctx and use that for the
|
||||
* req value to allow _fetchData to
|
||||
* work seemlessly in getInitialProps() on server side
|
||||
* pages *and* in _app.js.
|
||||
*/
|
||||
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()
|
||||
return Object.keys(data).length > 0 ? data : null // Return null if data empty
|
||||
} catch (error) {
|
||||
logger.error('CLIENT_FETCH_ERROR', path, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
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') }
|
||||
if (!process.env.NEXTAUTH_URL) {
|
||||
logger.warn('NEXTAUTH_URL', 'NEXTAUTH_URL environment variable not set')
|
||||
}
|
||||
|
||||
// Return absolute path when called server side
|
||||
return `${__NEXTAUTH.baseUrl}${__NEXTAUTH.basePath}`
|
||||
} else {
|
||||
// Return relative path when called client side
|
||||
return __NEXTAUTH.basePath
|
||||
return `${__NEXTAUTH.baseUrlServer}${__NEXTAUTH.basePathServer}`
|
||||
}
|
||||
// Return relative path when called client side
|
||||
return __NEXTAUTH.basePath
|
||||
}
|
||||
|
||||
const _encodedForm = (formData) => {
|
||||
return Object.keys(formData).map((key) => {
|
||||
return encodeURIComponent(key) + '=' + encodeURIComponent(formData[key])
|
||||
}).join('&')
|
||||
/** Returns the number of seconds elapsed since January 1, 1970 00:00:00 UTC. */
|
||||
function _now () {
|
||||
return Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
const _sendMessage = (message) => {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
const timestamp = Math.floor(new Date().getTime() / 1000)
|
||||
localStorage.setItem('nextauth.message', JSON.stringify({ ...message, clientId: __NEXTAUTH._clientId, timestamp })) // eslint-disable-line
|
||||
/**
|
||||
* Inspired by [Broadcast Channel API](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API)
|
||||
* Only not using it directly, because Safari does not support it.
|
||||
*
|
||||
* https://caniuse.com/?search=broadcastchannel
|
||||
*/
|
||||
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) => {
|
||||
if (event.key !== name) return
|
||||
/** @type {import("types/internals/client").BroadcastMessage} */
|
||||
const message = JSON.parse(event.newValue)
|
||||
if (message?.event !== 'session' || !message?.data) return
|
||||
|
||||
onReceive(message)
|
||||
})
|
||||
},
|
||||
/** Notify other tabs/windows. */
|
||||
post (message) {
|
||||
if (typeof localStorage === 'undefined') return
|
||||
localStorage.setItem(name,
|
||||
JSON.stringify({ ...message, timestamp: _now() })
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,44 @@
|
||||
:root {
|
||||
--color-background: #fff;
|
||||
--color-primary: #444;
|
||||
--color-control-border: #bbb;
|
||||
--color-button-active-background: #f9f9f9;
|
||||
--color-button-active-border: #aaa;
|
||||
--border-width: 1px;
|
||||
--border-radius: .3rem;
|
||||
--color-error: #c94b4b;
|
||||
--color-info: #157efb;
|
||||
--color-info-text: #fff;
|
||||
}
|
||||
|
||||
.__next-auth-theme-auto,
|
||||
.__next-auth-theme-light {
|
||||
--color-background: #fff;
|
||||
--color-text: #000;
|
||||
--color-primary: #444;
|
||||
--color-control-border: #bbb;
|
||||
--color-button-active-background: #f9f9f9;
|
||||
--color-button-active-border: #aaa;
|
||||
--color-seperator: #ccc;
|
||||
}
|
||||
|
||||
.__next-auth-theme-dark {
|
||||
--color-background: #000;
|
||||
--color-text: #fff;
|
||||
--color-primary: #ccc;
|
||||
--color-control-border: #555;
|
||||
--color-button-active-background: #060606;
|
||||
--color-button-active-border: #666;
|
||||
--color-seperator: #444;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.__next-auth-theme-auto {
|
||||
--color-background: #000;
|
||||
--color-text: #fff;
|
||||
--color-primary: #ccc;
|
||||
--color-control-border: #555;
|
||||
--color-button-active-background: #060606;
|
||||
--color-button-active-border: #666;
|
||||
--color-seperator: #444;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--color-background);
|
||||
margin: 0;
|
||||
@@ -22,6 +50,11 @@ h1 {
|
||||
font-weight: 400;
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 0 1rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--color-text)
|
||||
}
|
||||
|
||||
form {
|
||||
@@ -46,7 +79,8 @@ input[type] {
|
||||
background: var(--color-background);
|
||||
font-size: 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: inset 0 .1rem .2rem rgba(0,0,0,.2);
|
||||
box-shadow: inset 0 .1rem .2rem rgba(0, 0, 0, .2);
|
||||
color: var(--color-text);
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
@@ -63,6 +97,7 @@ p {
|
||||
a.button {
|
||||
text-decoration: none;
|
||||
line-height: 1rem;
|
||||
|
||||
&:link,
|
||||
&:visited {
|
||||
background-color: var(--color-background);
|
||||
@@ -79,17 +114,17 @@ a.button {
|
||||
background-color: var(--color-background);
|
||||
font-size: 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
transition: all .1s ease-in-out;
|
||||
box-shadow: 0 0.15rem 0.3rem rgba(0,0,0,.15), inset 0 .1rem .2rem var(--color-background), inset 0 -.1rem .1rem rgba(0,0,0,.05);
|
||||
transition: all .1s ease-in-out;
|
||||
box-shadow: 0 0.15rem 0.3rem rgba(0, 0, 0, .15), inset 0 .1rem .2rem var(--color-background), inset 0 -.1rem .1rem rgba(0, 0, 0, .05);
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
&:active {
|
||||
box-shadow: 0 0.15rem 0.3rem rgba(0,0,0,.15), inset 0 .1rem .2rem var(--color-background), inset 0 -.1rem .1rem rgba(0,0,0,.1);
|
||||
box-shadow: 0 0.15rem 0.3rem rgba(0, 0, 0, .15), inset 0 .1rem .2rem var(--color-background), inset 0 -.1rem .1rem rgba(0, 0, 0, .1);
|
||||
background-color: var(--color-button-active-background);
|
||||
border-color: var(--color-button-active-border);
|
||||
cursor: pointer;
|
||||
@@ -101,20 +136,21 @@ a.site {
|
||||
text-decoration: none;
|
||||
font-size: 1rem;
|
||||
line-height: 2rem;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.page {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: table;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
> div {
|
||||
>div {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
@@ -129,12 +165,14 @@ a.site {
|
||||
padding-right: 2rem;
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.signin {
|
||||
|
||||
button,
|
||||
a.button,
|
||||
input[type="text"] {
|
||||
@@ -165,25 +203,29 @@ a.site {
|
||||
font-weight: 500;
|
||||
border-radius: 0.3rem;
|
||||
background: var(--color-info);
|
||||
color: #fff;
|
||||
|
||||
p {
|
||||
text-align: left;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.2rem;
|
||||
color: var(--color-info-text);
|
||||
}
|
||||
}
|
||||
|
||||
> div,
|
||||
>div,
|
||||
form {
|
||||
display: block;
|
||||
margin: 0 auto 0.5rem auto;
|
||||
|
||||
input[type] {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
const pathToCss = path.join(__dirname, '/index.css')
|
||||
const css = fs.readFileSync(pathToCss, 'utf8')
|
||||
const pathToCss = path.join(process.cwd(), '/dist/css/index.css')
|
||||
|
||||
export default () => css
|
||||
export default function css () {
|
||||
return fs.readFileSync(pathToCss, 'utf8')
|
||||
}
|
||||
|
||||
@@ -1,41 +1,98 @@
|
||||
class UnknownError extends Error {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'UnknownError'
|
||||
this.message = message
|
||||
/**
|
||||
* Same as the default `Error`, but it is JSON serializable.
|
||||
* @source https://iaincollins.medium.com/error-handling-in-javascript-a6172ccdf9af
|
||||
*/
|
||||
export class UnknownError extends Error {
|
||||
constructor(error) {
|
||||
// Support passing error or string
|
||||
super(error?.message ?? error)
|
||||
this.name = "UnknownError"
|
||||
if (error instanceof Error) {
|
||||
this.stack = error.stack
|
||||
}
|
||||
}
|
||||
|
||||
toJSON () {
|
||||
toJSON() {
|
||||
return {
|
||||
error: {
|
||||
name: this.name,
|
||||
message: this.message
|
||||
// stack: this.stack
|
||||
}
|
||||
name: this.name,
|
||||
message: this.message,
|
||||
stack: this.stack,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CreateUserError extends UnknownError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'CreateUserError'
|
||||
this.message = message
|
||||
}
|
||||
export class OAuthCallbackError extends UnknownError {
|
||||
name = "OAuthCallbackError"
|
||||
}
|
||||
|
||||
// Thrown when an Email address is already associated with an account
|
||||
// but the user is trying an oAuth account that is not linked to it.
|
||||
class AccountNotLinkedError extends UnknownError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'AccountNotLinkedError'
|
||||
this.message = message
|
||||
}
|
||||
/**
|
||||
* Thrown when an Email address is already associated with an account
|
||||
* but the user is trying an OAuth account that is not linked to it.
|
||||
*/
|
||||
export class AccountNotLinkedError extends UnknownError {
|
||||
name = "AccountNotLinkedError"
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
UnknownError,
|
||||
CreateUserError,
|
||||
AccountNotLinkedError
|
||||
export class CreateUserError extends UnknownError {
|
||||
name = "CreateUserError"
|
||||
}
|
||||
|
||||
export class GetUserError extends UnknownError {
|
||||
name = "GetUserError"
|
||||
}
|
||||
|
||||
export class GetUserByEmailError extends UnknownError {
|
||||
name = "GetUserByEmailError"
|
||||
}
|
||||
|
||||
export class GetUserByIdError extends UnknownError {
|
||||
name = "GetUserByIdError"
|
||||
}
|
||||
|
||||
export class GetUserByProviderAccountIdError extends UnknownError {
|
||||
name = "GetUserByProviderAccountIdError"
|
||||
}
|
||||
|
||||
export class UpdateUserError extends UnknownError {
|
||||
name = "UpdateUserError"
|
||||
}
|
||||
|
||||
export class DeleteUserError extends UnknownError {
|
||||
name = "DeleteUserError"
|
||||
}
|
||||
|
||||
export class LinkAccountError extends UnknownError {
|
||||
name = "LinkAccountError"
|
||||
}
|
||||
|
||||
export class UnlinkAccountError extends UnknownError {
|
||||
name = "UnlinkAccountError"
|
||||
}
|
||||
|
||||
export class CreateSessionError extends UnknownError {
|
||||
name = "CreateSessionError"
|
||||
}
|
||||
|
||||
export class GetSessionError extends UnknownError {
|
||||
name = "GetSessionError"
|
||||
}
|
||||
|
||||
export class UpdateSessionError extends UnknownError {
|
||||
name = "UpdateSessionError"
|
||||
}
|
||||
|
||||
export class DeleteSessionError extends UnknownError {
|
||||
name = "DeleteSessionError"
|
||||
}
|
||||
|
||||
export class CreateVerificationRequestError extends UnknownError {
|
||||
name = "CreateVerificationRequestError"
|
||||
}
|
||||
|
||||
export class GetVerificationRequestError extends UnknownError {
|
||||
name = "GetVerificationRequestError"
|
||||
}
|
||||
|
||||
export class DeleteVerificationRequestError extends UnknownError {
|
||||
name = "DeleteVerificationRequestError"
|
||||
}
|
||||
|
||||
137
src/lib/jwt.js
137
src/lib/jwt.js
@@ -1,36 +1,36 @@
|
||||
import jose from 'jose'
|
||||
import hkdf from 'futoin-hkdf'
|
||||
import logger from './logger'
|
||||
import crypto from "crypto"
|
||||
import jose from "jose"
|
||||
import logger from "./logger"
|
||||
|
||||
// Set default algorithm to use for auto-generated signing key
|
||||
const DEFAULT_SIGNATURE_ALGORITHM = 'HS512'
|
||||
const DEFAULT_SIGNATURE_ALGORITHM = "HS512"
|
||||
|
||||
// Set default algorithm for auto-generated symmetric encryption key
|
||||
const DEFAULT_ENCRYPTION_ALGORITHM = 'A256GCM'
|
||||
const DEFAULT_ENCRYPTION_ALGORITHM = "A256GCM"
|
||||
|
||||
// Use encryption or not by default
|
||||
const DEFAULT_ENCRYPTION_ENABLED = false
|
||||
|
||||
const DEFAULT_MAX_AGE = 30 * 24 * 60 * 60 // 30 days
|
||||
|
||||
const encode = async ({
|
||||
export async function encode({
|
||||
token = {},
|
||||
maxAge = DEFAULT_MAX_AGE,
|
||||
secret,
|
||||
signingKey,
|
||||
signingOptions = {
|
||||
expiresIn: `${maxAge}s`
|
||||
expiresIn: `${maxAge}s`,
|
||||
},
|
||||
encryptionKey,
|
||||
encryptionOptions = {
|
||||
alg: 'dir',
|
||||
alg: "dir",
|
||||
enc: DEFAULT_ENCRYPTION_ALGORITHM,
|
||||
zip: 'DEF'
|
||||
zip: "DEF",
|
||||
},
|
||||
encryption = DEFAULT_ENCRYPTION_ENABLED
|
||||
} = {}) => {
|
||||
encryption = DEFAULT_ENCRYPTION_ENABLED,
|
||||
} = {}) {
|
||||
// Signing Key
|
||||
const _signingKey = (signingKey)
|
||||
const _signingKey = signingKey
|
||||
? jose.JWK.asKey(JSON.parse(signingKey))
|
||||
: getDerivedSigningKey(secret)
|
||||
|
||||
@@ -39,18 +39,17 @@ const encode = async ({
|
||||
|
||||
if (encryption) {
|
||||
// Encryption Key
|
||||
const _encryptionKey = (encryptionKey)
|
||||
const _encryptionKey = encryptionKey
|
||||
? jose.JWK.asKey(JSON.parse(encryptionKey))
|
||||
: getDerivedEncryptionKey(secret)
|
||||
|
||||
// Encrypt token
|
||||
return jose.JWE.encrypt(signedToken, _encryptionKey, encryptionOptions)
|
||||
} else {
|
||||
return signedToken
|
||||
}
|
||||
return signedToken
|
||||
}
|
||||
|
||||
const decode = async ({
|
||||
export async function decode({
|
||||
secret,
|
||||
token,
|
||||
maxAge = DEFAULT_MAX_AGE,
|
||||
@@ -58,32 +57,36 @@ const decode = async ({
|
||||
verificationKey = signingKey, // Optional (defaults to encryptionKey)
|
||||
verificationOptions = {
|
||||
maxTokenAge: `${maxAge}s`,
|
||||
algorithms: [DEFAULT_SIGNATURE_ALGORITHM]
|
||||
algorithms: [DEFAULT_SIGNATURE_ALGORITHM],
|
||||
},
|
||||
encryptionKey,
|
||||
decryptionKey = encryptionKey, // Optional (defaults to encryptionKey)
|
||||
decryptionOptions = {
|
||||
algorithms: [DEFAULT_ENCRYPTION_ALGORITHM]
|
||||
algorithms: [DEFAULT_ENCRYPTION_ALGORITHM],
|
||||
},
|
||||
encryption = DEFAULT_ENCRYPTION_ENABLED
|
||||
} = {}) => {
|
||||
encryption = DEFAULT_ENCRYPTION_ENABLED,
|
||||
} = {}) {
|
||||
if (!token) return null
|
||||
|
||||
let tokenToVerify = token
|
||||
|
||||
if (encryption) {
|
||||
// Encryption Key
|
||||
const _encryptionKey = (decryptionKey)
|
||||
const _encryptionKey = decryptionKey
|
||||
? jose.JWK.asKey(JSON.parse(decryptionKey))
|
||||
: getDerivedEncryptionKey(secret)
|
||||
|
||||
// Decrypt token
|
||||
const decryptedToken = jose.JWE.decrypt(token, _encryptionKey, decryptionOptions)
|
||||
tokenToVerify = decryptedToken.toString('utf8')
|
||||
const decryptedToken = jose.JWE.decrypt(
|
||||
token,
|
||||
_encryptionKey,
|
||||
decryptionOptions
|
||||
)
|
||||
tokenToVerify = decryptedToken.toString("utf8")
|
||||
}
|
||||
|
||||
// Signing Key
|
||||
const _signingKey = (verificationKey)
|
||||
const _signingKey = verificationKey
|
||||
? jose.JWK.asKey(JSON.parse(verificationKey))
|
||||
: getDerivedSigningKey(secret)
|
||||
|
||||
@@ -91,16 +94,31 @@ const decode = async ({
|
||||
return jose.JWT.verify(tokenToVerify, _signingKey, verificationOptions)
|
||||
}
|
||||
|
||||
const getToken = async (args) => {
|
||||
/**
|
||||
* Server-side method to retrieve the JWT from `req`.
|
||||
* @param {{
|
||||
* req: NextApiRequest
|
||||
* secureCookie?: boolean
|
||||
* cookieName?: string
|
||||
* raw?: boolean
|
||||
* }} params
|
||||
*/
|
||||
export async function getToken(params) {
|
||||
const {
|
||||
req,
|
||||
// Use secure prefix for cookie name, unless URL is NEXTAUTH_URL is http://
|
||||
// or not set (e.g. development or test instance) case use unprefixed name
|
||||
secureCookie = !(!process.env.NEXTAUTH_URL || process.env.NEXTAUTH_URL.startsWith('http://')),
|
||||
cookieName = (secureCookie) ? '__Secure-next-auth.session-token' : 'next-auth.session-token',
|
||||
raw = false
|
||||
} = args
|
||||
if (!req) throw new Error('Must pass `req` to JWT getToken()')
|
||||
secureCookie = !(
|
||||
!process.env.NEXTAUTH_URL ||
|
||||
process.env.NEXTAUTH_URL.startsWith("http://")
|
||||
),
|
||||
cookieName = secureCookie
|
||||
? "__Secure-next-auth.session-token"
|
||||
: "next-auth.session-token",
|
||||
raw = false,
|
||||
decode: _decode = decode,
|
||||
} = params
|
||||
if (!req) throw new Error("Must pass `req` to JWT getToken()")
|
||||
|
||||
// Try to get token from cookie
|
||||
let token = req.cookies[cookieName]
|
||||
@@ -108,8 +126,8 @@ const getToken = async (args) => {
|
||||
// If cookie not found in cookie look for bearer token in authorization header.
|
||||
// This allows clients that pass through tokens in headers rather than as
|
||||
// cookies to use this helper function.
|
||||
if (!token && req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') {
|
||||
const urlEncodedToken = req.headers.authorization.split(' ')[1]
|
||||
if (!token && req.headers.authorization?.split(" ")[0] === "Bearer") {
|
||||
const urlEncodedToken = req.headers.authorization.split(" ")[1]
|
||||
token = decodeURIComponent(urlEncodedToken)
|
||||
}
|
||||
|
||||
@@ -118,8 +136,8 @@ const getToken = async (args) => {
|
||||
}
|
||||
|
||||
try {
|
||||
return await decode({ token, ...args })
|
||||
} catch (error) {
|
||||
return _decode({ token, ...params })
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -128,30 +146,63 @@ const getToken = async (args) => {
|
||||
let DERIVED_SIGNING_KEY_WARNING = false
|
||||
let DERIVED_ENCRYPTION_KEY_WARNING = false
|
||||
|
||||
const getDerivedSigningKey = (secret) => {
|
||||
// Do the better hkdf of Node.js one added in `v15.0.0` and Third Party one
|
||||
function hkdf(secret, { byteLength, encryptionInfo, digest = "sha256" }) {
|
||||
if (crypto.hkdfSync) {
|
||||
return Buffer.from(
|
||||
crypto.hkdfSync(
|
||||
digest,
|
||||
secret,
|
||||
Buffer.alloc(0),
|
||||
encryptionInfo,
|
||||
byteLength
|
||||
)
|
||||
)
|
||||
}
|
||||
return require("futoin-hkdf")(secret, byteLength, {
|
||||
info: encryptionInfo,
|
||||
hash: digest,
|
||||
})
|
||||
}
|
||||
|
||||
function getDerivedSigningKey(secret) {
|
||||
if (!DERIVED_SIGNING_KEY_WARNING) {
|
||||
logger.warn('JWT_AUTO_GENERATED_SIGNING_KEY')
|
||||
logger.warn("JWT_AUTO_GENERATED_SIGNING_KEY")
|
||||
DERIVED_SIGNING_KEY_WARNING = true
|
||||
}
|
||||
|
||||
const buffer = hkdf(secret, 64, { info: 'NextAuth.js Generated Signing Key', hash: 'SHA-256' })
|
||||
const key = jose.JWK.asKey(buffer, { alg: DEFAULT_SIGNATURE_ALGORITHM, use: 'sig', kid: 'nextauth-auto-generated-signing-key' })
|
||||
const buffer = hkdf(secret, {
|
||||
byteLength: 64,
|
||||
encryptionInfo: "NextAuth.js Generated Signing Key",
|
||||
})
|
||||
const key = jose.JWK.asKey(buffer, {
|
||||
alg: DEFAULT_SIGNATURE_ALGORITHM,
|
||||
use: "sig",
|
||||
kid: "nextauth-auto-generated-signing-key",
|
||||
})
|
||||
return key
|
||||
}
|
||||
|
||||
const getDerivedEncryptionKey = (secret) => {
|
||||
function getDerivedEncryptionKey(secret) {
|
||||
if (!DERIVED_ENCRYPTION_KEY_WARNING) {
|
||||
logger.warn('JWT_AUTO_GENERATED_ENCRYPTION_KEY')
|
||||
logger.warn("JWT_AUTO_GENERATED_ENCRYPTION_KEY")
|
||||
DERIVED_ENCRYPTION_KEY_WARNING = true
|
||||
}
|
||||
|
||||
const buffer = hkdf(secret, 32, { info: 'NextAuth.js Generated Encryption Key', hash: 'SHA-256' })
|
||||
const key = jose.JWK.asKey(buffer, { alg: DEFAULT_ENCRYPTION_ALGORITHM, use: 'enc', kid: 'nextauth-auto-generated-encryption-key' })
|
||||
const buffer = hkdf(secret, {
|
||||
byteLength: 32,
|
||||
encryptionInfo: "NextAuth.js Generated Encryption Key",
|
||||
})
|
||||
const key = jose.JWK.asKey(buffer, {
|
||||
alg: DEFAULT_ENCRYPTION_ALGORITHM,
|
||||
use: "enc",
|
||||
kid: "nextauth-auto-generated-encryption-key",
|
||||
})
|
||||
return key
|
||||
}
|
||||
|
||||
export default {
|
||||
encode,
|
||||
decode,
|
||||
getToken
|
||||
getToken,
|
||||
}
|
||||
|
||||
@@ -1,32 +1,81 @@
|
||||
const logger = {
|
||||
error: (errorCode, ...text) => {
|
||||
if (!console) { return }
|
||||
if (text && text.length <= 1) { text = text[0] || '' }
|
||||
/** @type {import("types").LoggerInstance} */
|
||||
const _logger = {
|
||||
error(code, ...message) {
|
||||
console.error(
|
||||
`[next-auth][error][${errorCode.toLowerCase()}]`,
|
||||
text,
|
||||
`\nhttps://next-auth.js.org/errors#${errorCode.toLowerCase()}`
|
||||
`[next-auth][error][${code.toLowerCase()}]`,
|
||||
`\nhttps://next-auth.js.org/errors#${code.toLowerCase()}`,
|
||||
...message
|
||||
)
|
||||
},
|
||||
warn: (warnCode, ...text) => {
|
||||
if (!console) { return }
|
||||
if (text && text.length <= 1) { text = text[0] || '' }
|
||||
warn(code, ...message) {
|
||||
console.warn(
|
||||
`[next-auth][warn][${warnCode.toLowerCase()}]`,
|
||||
text,
|
||||
`\nhttps://next-auth.js.org/warning#${warnCode.toLowerCase()}`
|
||||
`[next-auth][warn][${code.toLowerCase()}]`,
|
||||
`\nhttps://next-auth.js.org/warnings#${code.toLowerCase()}`,
|
||||
...message
|
||||
)
|
||||
},
|
||||
debug: (debugCode, ...text) => {
|
||||
if (!console) { return }
|
||||
if (text && text.length <= 1) { text = text[0] || '' }
|
||||
if (process && process.env && process.env._NEXTAUTH_DEBUG) {
|
||||
console.log(
|
||||
`[next-auth][debug][${debugCode.toLowerCase()}]`,
|
||||
text
|
||||
)
|
||||
}
|
||||
}
|
||||
debug(code, ...message) {
|
||||
if (!process?.env?._NEXTAUTH_DEBUG) return
|
||||
console.log(`[next-auth][debug][${code.toLowerCase()}]`, ...message)
|
||||
},
|
||||
}
|
||||
|
||||
export default logger
|
||||
/**
|
||||
* Override the built-in logger.
|
||||
* Any `undefined` level will use the default logger.
|
||||
* @param {Partial<import("types").LoggerInstance>} newLogger
|
||||
*/
|
||||
export function setLogger(newLogger = {}) {
|
||||
if (newLogger.error) _logger.error = newLogger.error
|
||||
if (newLogger.warn) _logger.warn = newLogger.warn
|
||||
if (newLogger.debug) _logger.debug = newLogger.debug
|
||||
}
|
||||
|
||||
export default _logger
|
||||
|
||||
/**
|
||||
* Serializes client-side log messages and sends them to the server
|
||||
* @param {import("types").LoggerInstance} logger
|
||||
* @param {string} basePath
|
||||
* @return {import("types").LoggerInstance}
|
||||
*/
|
||||
export function proxyLogger(logger = _logger, basePath) {
|
||||
try {
|
||||
if (typeof window === "undefined") {
|
||||
return logger
|
||||
}
|
||||
|
||||
const clientLogger = {}
|
||||
for (const level in logger) {
|
||||
clientLogger[level] = (code, ...message) => {
|
||||
_logger[level](code, ...message) // Log on client as usual
|
||||
|
||||
const url = `${basePath}/_log`
|
||||
const body = new URLSearchParams({
|
||||
level,
|
||||
code,
|
||||
message: JSON.stringify(
|
||||
message.map((m) => {
|
||||
if (m instanceof Error) {
|
||||
// Serializing errors: https://iaincollins.medium.com/error-handling-in-javascript-a6172ccdf9af
|
||||
return { name: m.name, message: m.message, stack: m.stack }
|
||||
}
|
||||
return m
|
||||
})
|
||||
),
|
||||
})
|
||||
if (navigator.sendBeacon) {
|
||||
return navigator.sendBeacon(url, body)
|
||||
}
|
||||
return fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body,
|
||||
})
|
||||
}
|
||||
}
|
||||
return clientLogger
|
||||
} catch {
|
||||
return _logger
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
// Simple universal (client/server) function to split host and path
|
||||
// We use this rather than a library because we need to use the same logic both
|
||||
// client and server side and we only need to parse out the host and path, while
|
||||
// supporting a default value, so a simple split is sufficent.
|
||||
export default (url) => {
|
||||
/**
|
||||
* Simple universal (client/server) function to split host and path
|
||||
* We use this rather than a library because we need to use the same logic both
|
||||
* client and server side and we only need to parse out the host and path, while
|
||||
* supporting a default value, so a simple split is sufficent.
|
||||
* @param {string} url
|
||||
*/
|
||||
export default function parseUrl (url) {
|
||||
// Default values
|
||||
const defaultHost = 'http://localhost:3000'
|
||||
const defaultPath = '/api/auth'
|
||||
@@ -10,7 +13,7 @@ export default (url) => {
|
||||
if (!url) { url = `${defaultHost}${defaultPath}` }
|
||||
|
||||
// Default to HTTPS if no protocol explictly specified
|
||||
const protocol = url.match(/^http?:\/\//) ? 'http' : 'https'
|
||||
const protocol = url.startsWith('http:') ? 'http' : 'https'
|
||||
|
||||
// Normalize URLs by stripping protocol and no trailing slash
|
||||
url = url.replace(/^https?:\/\//, '').replace(/\/$/, '')
|
||||
@@ -20,8 +23,5 @@ export default (url) => {
|
||||
const baseUrl = _host ? `${protocol}://${_host}` : defaultHost
|
||||
const basePath = _path.length > 0 ? `/${_path.join('/')}` : defaultPath
|
||||
|
||||
return {
|
||||
baseUrl,
|
||||
basePath
|
||||
}
|
||||
return { baseUrl, basePath }
|
||||
}
|
||||
|
||||
@@ -1,52 +1,34 @@
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
export default (options) => {
|
||||
export default function Apple(options) {
|
||||
return {
|
||||
id: 'apple',
|
||||
name: 'Apple',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: 'name email',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: 'https://appleid.apple.com/auth/token',
|
||||
authorizationUrl: 'https://appleid.apple.com/auth/authorize?response_type=code&id_token&response_mode=form_post',
|
||||
id: "apple",
|
||||
name: "Apple",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "name email",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://appleid.apple.com/auth/token",
|
||||
authorizationUrl:
|
||||
"https://appleid.apple.com/auth/authorize?response_type=code&id_token&response_mode=form_post",
|
||||
profileUrl: null,
|
||||
idToken: true,
|
||||
state: false, // Apple doesn't support state verfication
|
||||
profile: (profile) => {
|
||||
profile(profile) {
|
||||
// The name of the user will only return on first login
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.user != null ? profile.user.name.firstName + ' ' + profile.user.name.lastName : null,
|
||||
email: profile.email
|
||||
name:
|
||||
profile.user != null
|
||||
? profile.user.name.firstName + " " + profile.user.name.lastName
|
||||
: null,
|
||||
email: profile.email,
|
||||
}
|
||||
},
|
||||
clientId: null,
|
||||
clientSecret: {
|
||||
appleId: null,
|
||||
teamId: null,
|
||||
privateKey: null,
|
||||
keyId: null
|
||||
keyId: null,
|
||||
},
|
||||
clientSecretCallback: async ({ appleId, keyId, teamId, privateKey }) => {
|
||||
const response = jwt.sign(
|
||||
{
|
||||
iss: teamId,
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
exp: Math.floor(Date.now() / 1000) + (86400 * 180), // 6 months
|
||||
aud: 'https://appleid.apple.com',
|
||||
sub: appleId
|
||||
},
|
||||
// Automatically convert \\n into \n if found in private key. If the key
|
||||
// is passed in an environment variable \n can get escaped as \\n
|
||||
privateKey.replace(/\\n/g, '\n'),
|
||||
{
|
||||
algorithm: 'ES256',
|
||||
keyid: keyId
|
||||
}
|
||||
)
|
||||
return Promise.resolve(response)
|
||||
},
|
||||
...options
|
||||
protection: "none", // REVIEW: Apple does not support state, as far as I know. Can we use "pkce" then?
|
||||
...options,
|
||||
}
|
||||
}
|
||||
|
||||
24
src/providers/atlassian.js
Normal file
24
src/providers/atlassian.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export default function Atlassian(options) {
|
||||
return {
|
||||
id: "atlassian",
|
||||
name: "Atlassian",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
params: {
|
||||
grant_type: "authorization_code",
|
||||
},
|
||||
accessTokenUrl: "https://auth.atlassian.com/oauth/token",
|
||||
authorizationUrl:
|
||||
"https://auth.atlassian.com/authorize?audience=api.atlassian.com&response_type=code&prompt=consent",
|
||||
profileUrl: "https://api.atlassian.com/me",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.account_id,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: profile.picture,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,22 @@
|
||||
export default (options) => {
|
||||
export default function Auth0(options) {
|
||||
return {
|
||||
id: 'auth0',
|
||||
name: 'Auth0',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
scope: 'openid email profile',
|
||||
id: "auth0",
|
||||
name: "Auth0",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
params: { grant_type: "authorization_code" },
|
||||
scope: "openid email profile",
|
||||
accessTokenUrl: `https://${options.domain}/oauth/token`,
|
||||
authorizationUrl: `https://${options.domain}/authorize?response_type=code`,
|
||||
profileUrl: `https://${options.domain}/userinfo`,
|
||||
profile: (profile) => {
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.nickname,
|
||||
email: profile.email,
|
||||
image: profile.picture
|
||||
image: profile.picture,
|
||||
}
|
||||
},
|
||||
...options
|
||||
...options,
|
||||
}
|
||||
}
|
||||
|
||||
24
src/providers/azure-ad-b2c.js
Normal file
24
src/providers/azure-ad-b2c.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export default function AzureADB2C(options) {
|
||||
const tenant = options.tenantId ? options.tenantId : "common"
|
||||
|
||||
return {
|
||||
id: "azure-ad-b2c",
|
||||
name: "Azure Active Directory B2C",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
params: {
|
||||
grant_type: "authorization_code",
|
||||
},
|
||||
accessTokenUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
|
||||
authorizationUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize?response_type=code&response_mode=query`,
|
||||
profileUrl: "https://graph.microsoft.com/v1.0/me/",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.displayName,
|
||||
email: profile.userPrincipalName,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
}
|
||||
22
src/providers/basecamp.js
Normal file
22
src/providers/basecamp.js
Normal file
@@ -0,0 +1,22 @@
|
||||
export default function Basecamp(options) {
|
||||
return {
|
||||
id: "basecamp",
|
||||
name: "Basecamp",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
accessTokenUrl:
|
||||
"https://launchpad.37signals.com/authorization/token?type=web_server",
|
||||
authorizationUrl:
|
||||
"https://launchpad.37signals.com/authorization/new?type=web_server",
|
||||
profileUrl: "https://launchpad.37signals.com/authorization.json",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.identity.id,
|
||||
name: `${profile.identity.first_name} ${profile.identity.last_name}`,
|
||||
email: profile.identity.email_address,
|
||||
image: null,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,29 @@
|
||||
export default (options) => {
|
||||
export default function BattleNet(options) {
|
||||
const { region } = options
|
||||
return {
|
||||
id: 'battlenet',
|
||||
name: 'Battle.net',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: 'openid',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
id: "battlenet",
|
||||
name: "Battle.net",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "openid",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl:
|
||||
region === 'CN'
|
||||
? 'https://www.battlenet.com.cn/oauth/token'
|
||||
region === "CN"
|
||||
? "https://www.battlenet.com.cn/oauth/token"
|
||||
: `https://${region}.battle.net/oauth/token`,
|
||||
authorizationUrl:
|
||||
region === 'CN'
|
||||
? 'https://www.battlenet.com.cn/oauth/authorize'
|
||||
: `https://${region}.battle.net/oauth/authorize`,
|
||||
profileUrl: 'https://us.battle.net/oauth/userinfo',
|
||||
profile: (profile) => {
|
||||
region === "CN"
|
||||
? "https://www.battlenet.com.cn/oauth/authorize?response_type=code"
|
||||
: `https://${region}.battle.net/oauth/authorize?response_type=code`,
|
||||
profileUrl: "https://us.battle.net/oauth/userinfo",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.battletag,
|
||||
email: null,
|
||||
image: null
|
||||
image: null,
|
||||
}
|
||||
},
|
||||
...options
|
||||
...options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
export default (options) => {
|
||||
export default function Box(options) {
|
||||
return {
|
||||
id: 'box',
|
||||
name: 'Box',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: '',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: 'https://api.box.com/oauth2/token',
|
||||
authorizationUrl: 'https://account.box.com/api/oauth2/authorize?response_type=code',
|
||||
profileUrl: 'https://api.box.com/2.0/users/me',
|
||||
profile: (profile) => {
|
||||
id: "box",
|
||||
name: "Box",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://api.box.com/oauth2/token",
|
||||
authorizationUrl:
|
||||
"https://account.box.com/api/oauth2/authorize?response_type=code",
|
||||
profileUrl: "https://api.box.com/2.0/users/me",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.name,
|
||||
email: profile.login,
|
||||
image: profile.avatar_url
|
||||
image: profile.avatar_url,
|
||||
}
|
||||
},
|
||||
...options
|
||||
...options,
|
||||
}
|
||||
}
|
||||
|
||||
34
src/providers/bungie.js
Normal file
34
src/providers/bungie.js
Normal file
@@ -0,0 +1,34 @@
|
||||
export default function Bungie(options) {
|
||||
return {
|
||||
id: "bungie",
|
||||
name: "Bungie",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "",
|
||||
params: { reauth: "true", grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://www.bungie.net/platform/app/oauth/token/",
|
||||
requestTokenUrl: "https://www.bungie.net/platform/app/oauth/token/",
|
||||
authorizationUrl:
|
||||
"https://www.bungie.net/en/OAuth/Authorize?response_type=code",
|
||||
profileUrl:
|
||||
"https://www.bungie.net/platform/User/GetBungieAccount/{membershipId}/254/",
|
||||
profile(profile) {
|
||||
const { bungieNetUser: user } = profile.Response
|
||||
|
||||
return {
|
||||
id: user.membershipId,
|
||||
name: user.displayName,
|
||||
image: `https://www.bungie.net${
|
||||
user.profilePicturePath.startsWith("/") ? "" : "/"
|
||||
}${user.profilePicturePath}`,
|
||||
email: null,
|
||||
}
|
||||
},
|
||||
headers: {
|
||||
"X-API-Key": null,
|
||||
},
|
||||
clientId: null,
|
||||
clientSecret: null,
|
||||
...options,
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,23 @@
|
||||
export default (options) => {
|
||||
export default function Cognito(options) {
|
||||
const { domain } = options
|
||||
return {
|
||||
id: 'cognito',
|
||||
name: 'Cognito',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: 'openid profile email',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
id: "cognito",
|
||||
name: "Cognito",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "openid profile email",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: `https://${domain}/oauth2/token`,
|
||||
authorizationUrl: `https://${domain}/oauth2/authorize?response_type=code`,
|
||||
profileUrl: `https://${domain}/oauth2/userInfo`,
|
||||
profile: (profile) => {
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.username,
|
||||
email: profile.email,
|
||||
image: null
|
||||
image: null,
|
||||
}
|
||||
},
|
||||
...options
|
||||
...options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
export default (options) => {
|
||||
export default function Credentials(options) {
|
||||
return {
|
||||
id: 'credentials',
|
||||
name: 'Credentials',
|
||||
type: 'credentials',
|
||||
id: "credentials",
|
||||
name: "Credentials",
|
||||
type: "credentials",
|
||||
authorize: null,
|
||||
credentials: null,
|
||||
...options
|
||||
...options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
export default (options) => {
|
||||
export default function Discord(options) {
|
||||
return {
|
||||
id: 'discord',
|
||||
name: 'Discord',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: 'identify email',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: 'https://discordapp.com/api/oauth2/token',
|
||||
id: "discord",
|
||||
name: "Discord",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "identify email",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://discord.com/api/oauth2/token",
|
||||
authorizationUrl:
|
||||
'https://discordapp.com/api/oauth2/authorize?response_type=code&prompt=consent',
|
||||
profileUrl: 'https://discordapp.com/api/users/@me',
|
||||
profile: (profile) => {
|
||||
"https://discord.com/api/oauth2/authorize?response_type=code&prompt=none",
|
||||
profileUrl: "https://discord.com/api/users/@me",
|
||||
profile(profile) {
|
||||
if (profile.avatar === null) {
|
||||
const defaultAvatarNumber = parseInt(profile.discriminator) % 5
|
||||
profile.image_url = `https://cdn.discordapp.com/embed/avatars/${defaultAvatarNumber}.png`
|
||||
} else {
|
||||
const format = profile.avatar.startsWith("a_") ? "gif" : "png"
|
||||
profile.image_url = `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.${format}`
|
||||
}
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.username,
|
||||
image: `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.png`,
|
||||
email: profile.email
|
||||
image: profile.image_url,
|
||||
email: profile.email,
|
||||
}
|
||||
},
|
||||
...options
|
||||
...options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +1,54 @@
|
||||
import nodemailer from 'nodemailer'
|
||||
import logger from '../lib/logger'
|
||||
import nodemailer from "nodemailer"
|
||||
import logger from "../lib/logger"
|
||||
|
||||
export default (options) => {
|
||||
export default function Email(options) {
|
||||
return {
|
||||
id: 'email',
|
||||
type: 'email',
|
||||
name: 'Email',
|
||||
id: "email",
|
||||
type: "email",
|
||||
name: "Email",
|
||||
// Server can be an SMTP connection string or a nodemailer config object
|
||||
server: {
|
||||
host: 'localhost',
|
||||
host: "localhost",
|
||||
port: 25,
|
||||
auth: {
|
||||
user: '',
|
||||
pass: ''
|
||||
}
|
||||
user: "",
|
||||
pass: "",
|
||||
},
|
||||
},
|
||||
from: 'NextAuth <no-reply@example.com>',
|
||||
maxAge: 24 * 60 * 60, // How long email links are valid for (default 24h)
|
||||
from: "NextAuth <no-reply@example.com>",
|
||||
maxAge: 24 * 60 * 60,
|
||||
sendVerificationRequest,
|
||||
...options
|
||||
...options,
|
||||
}
|
||||
}
|
||||
|
||||
const sendVerificationRequest = ({ identifier: email, url, baseUrl, provider }) => {
|
||||
const sendVerificationRequest = ({
|
||||
identifier: email,
|
||||
url,
|
||||
baseUrl,
|
||||
provider,
|
||||
}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { server, from } = provider
|
||||
// Strip protocol from URL and use domain as site name
|
||||
const site = baseUrl.replace(/^https?:\/\//, '')
|
||||
const site = baseUrl.replace(/^https?:\/\//, "")
|
||||
|
||||
nodemailer
|
||||
.createTransport(server)
|
||||
.sendMail({
|
||||
nodemailer.createTransport(server).sendMail(
|
||||
{
|
||||
to: email,
|
||||
from,
|
||||
subject: `Sign in to ${site}`,
|
||||
text: text({ url, site, email }),
|
||||
html: html({ url, site, email })
|
||||
}, (error) => {
|
||||
html: html({ url, site, email }),
|
||||
},
|
||||
(error) => {
|
||||
if (error) {
|
||||
logger.error('SEND_VERIFICATION_EMAIL_ERROR', email, error)
|
||||
return reject(new Error('SEND_VERIFICATION_EMAIL_ERROR', error))
|
||||
logger.error("SEND_VERIFICATION_EMAIL_ERROR", email, error)
|
||||
return reject(new Error("SEND_VERIFICATION_EMAIL_ERROR", error))
|
||||
}
|
||||
return resolve()
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -52,16 +58,16 @@ const html = ({ url, site, email }) => {
|
||||
// email address and the domain from being turned into a hyperlink by email
|
||||
// clients like Outlook and Apple mail, as this is confusing because it seems
|
||||
// like they are supposed to click on their email address to sign in.
|
||||
const escapedEmail = `${email.replace(/\./g, '​.')}`
|
||||
const escapedSite = `${site.replace(/\./g, '​.')}`
|
||||
const escapedEmail = `${email.replace(/\./g, "​.")}`
|
||||
const escapedSite = `${site.replace(/\./g, "​.")}`
|
||||
|
||||
// Some simple styling options
|
||||
const backgroundColor = '#f9f9f9'
|
||||
const textColor = '#444444'
|
||||
const mainBackgroundColor = '#ffffff'
|
||||
const buttonBackgroundColor = '#346df1'
|
||||
const buttonBorderColor = '#346df1'
|
||||
const buttonTextColor = '#ffffff'
|
||||
const backgroundColor = "#f9f9f9"
|
||||
const textColor = "#444444"
|
||||
const mainBackgroundColor = "#ffffff"
|
||||
const buttonBackgroundColor = "#346df1"
|
||||
const buttonBorderColor = "#346df1"
|
||||
const buttonTextColor = "#ffffff"
|
||||
|
||||
return `
|
||||
<body style="background: ${backgroundColor};">
|
||||
|
||||
22
src/providers/eveonline.js
Normal file
22
src/providers/eveonline.js
Normal file
@@ -0,0 +1,22 @@
|
||||
export default function EVEOnline(options) {
|
||||
return {
|
||||
id: "eveonline",
|
||||
name: "EVE Online",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://login.eveonline.com/oauth/token",
|
||||
authorizationUrl:
|
||||
"https://login.eveonline.com/oauth/authorize?response_type=code",
|
||||
profileUrl: "https://login.eveonline.com/oauth/verify",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.CharacterID,
|
||||
name: profile.CharacterName,
|
||||
image: `https://image.eveonline.com/Character/${profile.CharacterID}_128.jpg`,
|
||||
email: null,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,22 @@
|
||||
export default (options) => {
|
||||
export default function Facebook(options) {
|
||||
return {
|
||||
id: 'facebook',
|
||||
name: 'Facebook',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: 'email',
|
||||
accessTokenUrl: 'https://graph.facebook.com/oauth/access_token',
|
||||
authorizationUrl: 'https://www.facebook.com/v7.0/dialog/oauth?response_type=code',
|
||||
profileUrl: 'https://graph.facebook.com/me?fields=email,name,picture',
|
||||
profile: (profile) => {
|
||||
id: "facebook",
|
||||
name: "Facebook",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "email",
|
||||
accessTokenUrl: "https://graph.facebook.com/oauth/access_token",
|
||||
authorizationUrl:
|
||||
"https://www.facebook.com/v7.0/dialog/oauth?response_type=code",
|
||||
profileUrl: "https://graph.facebook.com/me?fields=email,name,picture",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: profile.picture.data.url
|
||||
image: profile.picture.data.url,
|
||||
}
|
||||
},
|
||||
...options
|
||||
...options,
|
||||
}
|
||||
}
|
||||
|
||||
28
src/providers/faceit.js
Normal file
28
src/providers/faceit.js
Normal file
@@ -0,0 +1,28 @@
|
||||
export default function FACEIT(options) {
|
||||
return {
|
||||
id: "faceit",
|
||||
name: "FACEIT",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
params: { grant_type: "authorization_code" },
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from(
|
||||
`${options.clientId}:${options.clientSecret}`
|
||||
).toString("base64")}`,
|
||||
},
|
||||
accessTokenUrl: "https://api.faceit.com/auth/v1/oauth/token",
|
||||
authorizationUrl:
|
||||
"https://accounts.faceit.com/accounts?redirect_popup=true&response_type=code",
|
||||
profileUrl: "https://api.faceit.com/auth/v1/resources/userinfo",
|
||||
profile(profile) {
|
||||
const { guid: id, nickname: name, email, picture: image } = profile
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
email,
|
||||
image,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
}
|
||||
23
src/providers/foursquare.js
Normal file
23
src/providers/foursquare.js
Normal file
@@ -0,0 +1,23 @@
|
||||
export default function Foursquare(options) {
|
||||
const { apiVersion } = options
|
||||
return {
|
||||
id: "foursquare",
|
||||
name: "Foursquare",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://foursquare.com/oauth2/access_token",
|
||||
authorizationUrl:
|
||||
"https://foursquare.com/oauth2/authenticate?response_type=code",
|
||||
profileUrl: `https://api.foursquare.com/v2/users/self?v=${apiVersion}`,
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: `${profile.firstName} ${profile.lastName}`,
|
||||
image: `${profile.prefix}original${profile.suffix}`,
|
||||
email: profile.contact.email,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
}
|
||||
27
src/providers/fusionauth.js
Normal file
27
src/providers/fusionauth.js
Normal file
@@ -0,0 +1,27 @@
|
||||
export default function FusionAuth(options) {
|
||||
let authorizationUrl = `https://${options.domain}/oauth2/authorize?response_type=code`
|
||||
if (options.tenantId) {
|
||||
authorizationUrl += `&tenantId=${options.tenantId}`
|
||||
}
|
||||
|
||||
return {
|
||||
id: "fusionauth",
|
||||
name: "FusionAuth",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "openid",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: `https://${options.domain}/oauth2/token`,
|
||||
authorizationUrl,
|
||||
profileUrl: `https://${options.domain}/oauth2/userinfo`,
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: profile.picture,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
export default (options) => {
|
||||
export default function GitHub(options) {
|
||||
return {
|
||||
id: 'github',
|
||||
name: 'GitHub',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: 'user',
|
||||
accessTokenUrl: 'https://github.com/login/oauth/access_token',
|
||||
authorizationUrl: 'https://github.com/login/oauth/authorize',
|
||||
profileUrl: 'https://api.github.com/user',
|
||||
profile: (profile) => {
|
||||
id: "github",
|
||||
name: "GitHub",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "user",
|
||||
accessTokenUrl: "https://github.com/login/oauth/access_token",
|
||||
authorizationUrl: "https://github.com/login/oauth/authorize",
|
||||
profileUrl: "https://api.github.com/user",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.name || profile.login,
|
||||
email: profile.email,
|
||||
image: profile.avatar_url
|
||||
image: profile.avatar_url,
|
||||
}
|
||||
},
|
||||
...options
|
||||
...options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
export default (options) => {
|
||||
export default function GitLab(options) {
|
||||
return {
|
||||
id: 'gitlab',
|
||||
name: 'GitLab',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: 'read_user',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: 'https://gitlab.com/oauth/token',
|
||||
authorizationUrl: 'https://gitlab.com/oauth/authorize?response_type=code',
|
||||
profileUrl: 'https://gitlab.com/api/v4/user',
|
||||
profile: (profile) => {
|
||||
id: "gitlab",
|
||||
name: "GitLab",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "read_user",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://gitlab.com/oauth/token",
|
||||
authorizationUrl: "https://gitlab.com/oauth/authorize?response_type=code",
|
||||
profileUrl: "https://gitlab.com/api/v4/user",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.username,
|
||||
email: profile.email,
|
||||
image: profile.avatar_url
|
||||
image: profile.avatar_url,
|
||||
}
|
||||
},
|
||||
...options
|
||||
...options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
export default (options) => {
|
||||
export default function Google(options) {
|
||||
return {
|
||||
id: 'google',
|
||||
name: 'Google',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: 'https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: 'https://accounts.google.com/o/oauth2/token',
|
||||
requestTokenUrl: 'https://accounts.google.com/o/oauth2/auth',
|
||||
authorizationUrl: 'https://accounts.google.com/o/oauth2/auth?response_type=code',
|
||||
profileUrl: 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json',
|
||||
profile: (profile) => {
|
||||
id: "google",
|
||||
name: "Google",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope:
|
||||
"https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://accounts.google.com/o/oauth2/token",
|
||||
requestTokenUrl: "https://accounts.google.com/o/oauth2/auth",
|
||||
authorizationUrl:
|
||||
"https://accounts.google.com/o/oauth2/auth?response_type=code",
|
||||
profileUrl: "https://www.googleapis.com/oauth2/v1/userinfo?alt=json",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: profile.picture
|
||||
image: profile.picture,
|
||||
}
|
||||
},
|
||||
...options
|
||||
...options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
export default (options) => {
|
||||
export default function IdentityServer4(options) {
|
||||
return {
|
||||
id: 'identity-server4',
|
||||
name: 'IdentityServer4',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: 'openid profile email',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
id: "identity-server4",
|
||||
name: "IdentityServer4",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "openid profile email",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: `https://${options.domain}/connect/token`,
|
||||
authorizationUrl: `https://${options.domain}/connect/authorize?response_type=code`,
|
||||
profileUrl: `https://${options.domain}/connect/userinfo`,
|
||||
profile: (profile) => {
|
||||
profile(profile) {
|
||||
return { ...profile, id: profile.sub }
|
||||
},
|
||||
setGetAccessTokenAuthHeader: false,
|
||||
...options
|
||||
...options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import Auth0 from './auth0'
|
||||
import Apple from './apple'
|
||||
import Box from './box'
|
||||
import Credentials from './credentials'
|
||||
import BattleNet from './battlenet'
|
||||
import Cognito from './cognito'
|
||||
import Discord from './discord'
|
||||
import Email from './email'
|
||||
import Facebook from './facebook'
|
||||
import GitHub from './github'
|
||||
import GitLab from './gitlab'
|
||||
import Google from './google'
|
||||
import IdentityServer4 from './identity-server4'
|
||||
import LinkedIn from './linkedin'
|
||||
import Mixer from './mixer'
|
||||
import Okta from './okta'
|
||||
import Slack from './slack'
|
||||
import Spotify from './spotify'
|
||||
import Twitch from './twitch'
|
||||
import Twitter from './twitter'
|
||||
import Yandex from './yandex'
|
||||
|
||||
export default {
|
||||
Auth0,
|
||||
Apple,
|
||||
Box,
|
||||
Credentials,
|
||||
BattleNet,
|
||||
Cognito,
|
||||
Discord,
|
||||
Email,
|
||||
Facebook,
|
||||
GitHub,
|
||||
GitLab,
|
||||
Google,
|
||||
IdentityServer4,
|
||||
LinkedIn,
|
||||
Mixer,
|
||||
Okta,
|
||||
Slack,
|
||||
Spotify,
|
||||
Twitter,
|
||||
Twitch,
|
||||
Yandex
|
||||
}
|
||||
50
src/providers/instagram.js
Normal file
50
src/providers/instagram.js
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @type {import("types/providers").OAuthProvider} options
|
||||
* @example
|
||||
*
|
||||
* ```js
|
||||
* // pages/api/auth/[...nextauth].js
|
||||
* import Providers from `next-auth/providers`
|
||||
* ...
|
||||
* providers: [
|
||||
* Providers.Instagram({
|
||||
* clientId: process.env.INSTAGRAM_CLIENT_ID,
|
||||
* clientSecret: process.env.INSTAGRAM_CLIENT_SECRET
|
||||
* })
|
||||
* ]
|
||||
* ...
|
||||
*
|
||||
* // pages/index
|
||||
* import { signIn } from "next-auth/client"
|
||||
* ...
|
||||
* <button onClick={() => signIn("instagram")}>
|
||||
* Sign in
|
||||
* </button>
|
||||
* ...
|
||||
* ```
|
||||
* [NextAuth.js Documentation](https://next-auth.js.org/providers/instagram) | [Instagram Documentation](https://developers.facebook.com/docs/instagram-basic-display-api/getting-started) | [Configuration](https://developers.facebook.com/apps)
|
||||
*/
|
||||
export default function Instagram(options) {
|
||||
return {
|
||||
id: "instagram",
|
||||
name: "Instagram",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "user_profile",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://api.instagram.com/oauth/access_token",
|
||||
authorizationUrl:
|
||||
"https://api.instagram.com/oauth/authorize?response_type=code",
|
||||
profileUrl:
|
||||
"https://graph.instagram.com/me?fields=id,username,account_type,name",
|
||||
async profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.username,
|
||||
email: null,
|
||||
image: null,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user