mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
49 Commits
next-auth@
...
v3.2.0-can
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf7efbc252 | ||
|
|
b9862b86b5 | ||
|
|
9b579b5fcb | ||
|
|
abcf845ebf | ||
|
|
ee398d1acd | ||
|
|
c31cbbcd30 | ||
|
|
1728f50952 | ||
|
|
2eb17cba1a | ||
|
|
15196ee3d1 | ||
|
|
aa4439e182 | ||
|
|
66ec439b4d | ||
|
|
a49068643c | ||
|
|
1a315fe5ac | ||
|
|
652ac7de35 | ||
|
|
28ce71d99e | ||
|
|
28e2afbd3a | ||
|
|
eb828d42f8 | ||
|
|
d03504c6ef | ||
|
|
8827950f12 | ||
|
|
4f89d74d78 | ||
|
|
be159b1b18 | ||
|
|
19f2664a78 | ||
|
|
bd86e7c7c7 | ||
|
|
7ce37c71d7 | ||
|
|
3c3a4d2c4f | ||
|
|
5fcf80ce81 | ||
|
|
7a4534a6b1 | ||
|
|
ddaa830e10 | ||
|
|
9dbd372f08 | ||
|
|
dde908b54a | ||
|
|
831c59dd5c | ||
|
|
3abb0c8223 | ||
|
|
8c56e13577 | ||
|
|
12d7856640 | ||
|
|
4635113133 | ||
|
|
1aea187d5e | ||
|
|
47b8788249 | ||
|
|
06a160aa0c | ||
|
|
93f4dc0622 | ||
|
|
6088a05204 | ||
|
|
d242d72106 | ||
|
|
766874dbd8 | ||
|
|
0b7343702f | ||
|
|
0327b9049a | ||
|
|
2ee460de00 | ||
|
|
c8de34d003 | ||
|
|
d15572074f | ||
|
|
7b6fd818a5 | ||
|
|
e031591468 |
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
|
||||
8
.github/ISSUE_TEMPLATE/question.md
vendored
8
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -4,12 +4,16 @@ about: Ask a question about NextAuth.js or for help using it
|
||||
labels: question
|
||||
assignees: ''
|
||||
---
|
||||
<!-- NOTE: Questions will be converted to Discussions. You can find them at https://github.com/nextauthjs/next-auth/discussions! -->
|
||||
|
||||
**Your question**
|
||||
A clear and concise question.
|
||||
<!-- A clear and concise question. -->
|
||||
|
||||
**What are you trying to do**
|
||||
A description of what you are trying to do, for context.
|
||||
<!-- A description of what you are trying to do, for context. -->
|
||||
|
||||
**Reproduction**
|
||||
<!-- If your question is code related, adding a reproduction to your use case can greatly reduce the time it takes us to figure out how to better help you. -->
|
||||
|
||||
**Feedback**
|
||||
*Documentation refers to searching through [online documentation](https://next-auth.js.org), code comments and issue history. The example project refers to [next-auth-example](https://github.com/iaincollins/next-auth-example).*
|
||||
|
||||
43
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
43
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
<!--
|
||||
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).
|
||||
|
||||
Note before creating the Pull Request. Even though the CONTRIBUTONG.md tells otherwise, we ask you to use the `canary` branch as base for your PR. We are tranistioning to a new structure, and the CONTRIBUTONG.md file has not been updated yet. Thank you!
|
||||
|
||||
If you're new to contributing to open source projects, you might find this free
|
||||
video course helpful: http://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 -->
|
||||
8
.github/stale.yml
vendored
8
.github/stale.yml
vendored
@@ -8,15 +8,17 @@ exemptLabels:
|
||||
- security
|
||||
- priority
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: wontfix
|
||||
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 ot open. Thanks!
|
||||
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. Thanks!
|
||||
and I will reopen it. (Read more at #912)
|
||||
Thanks!
|
||||
|
||||
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@@ -4,9 +4,13 @@ name: Build Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches:
|
||||
- main
|
||||
- canary
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
branches:
|
||||
- main
|
||||
- canary
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
34
.github/workflows/npm-publish.yml
vendored
34
.github/workflows/npm-publish.yml
vendored
@@ -1,34 +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 run build
|
||||
- run: npm run lint
|
||||
|
||||
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}}
|
||||
30
.github/workflows/release.yml
vendored
Normal file
30
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: Release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- canary
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
- name: Build
|
||||
run: npm run build
|
||||
- name: Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: npx semantic-release
|
||||
39
.releaserc.json
Normal file
39
.releaserc.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"branches": [
|
||||
"main",
|
||||
{ "name": "canary", "prerelease": true }
|
||||
],
|
||||
"plugins": [
|
||||
["@semantic-release/commit-analyzer", {
|
||||
"preset": "conventionalcommits",
|
||||
"releaseRules": [
|
||||
{ "breaking": true, "release": "major" },
|
||||
{ "revert": true, "release": "patch" },
|
||||
{ "type": "feat", "release": "minor" },
|
||||
{ "type": "fix", "release": "patch" },
|
||||
{ "type": "perf", "release": "patch" },
|
||||
{ "type": "docs", "release": "patch" }
|
||||
]
|
||||
}],
|
||||
["@semantic-release/release-notes-generator", {
|
||||
"preset": "conventionalcommits",
|
||||
"presetConfig": {
|
||||
"types": [
|
||||
{ "type": "feat", "section": "Features", "hidden": false },
|
||||
{ "type": "fix", "section": "Bug Fixes", "hidden": false },
|
||||
{ "type": "perf", "section": "Performance Improvements", "hidden": false },
|
||||
{ "type": "revert", "section": "Reverts", "hidden": false },
|
||||
{ "type": "docs", "section": "Documentation", "hidden": false },
|
||||
{ "type": "style", "section": "Styles", "hidden": false },
|
||||
{ "type": "chore", "section": "Miscellaneous Chores", "hidden": false },
|
||||
{ "type": "refactor", "section": "Code Refactoring", "hidden": false },
|
||||
{ "type": "test", "section": "Tests", "hidden": false },
|
||||
{ "type": "build", "section": "Build System", "hidden": false },
|
||||
{ "type": "ci", "section": "Continuous Integration", "hidden": false }
|
||||
]
|
||||
}
|
||||
}],
|
||||
"@semantic-release/github",
|
||||
"@semantic-release/npm"
|
||||
]
|
||||
}
|
||||
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).
|
||||
67
README.md
67
README.md
@@ -1,7 +1,20 @@
|
||||
# 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;">
|
||||
<img src="https://github.com/nextauthjs/next-auth/workflows/Build%20Test/badge.svg" alt="Build Test" />
|
||||
<img src="https://github.com/nextauthjs/next-auth/workflows/Integration%20Test/badge.svg" alt="Integration Test" />
|
||||
<img src="https://img.shields.io/bundlephobia/minzip/next-auth" alt="Bundle Size"/>
|
||||
<img src="https://img.shields.io/npm/dm/next-auth" alt="Downloads" />
|
||||
<img src="https://img.shields.io/github/stars/nextauthjs/next-auth" alt="Github Stars" />
|
||||
<img src="https://img.shields.io/github/v/release/nextauthjs/next-auth?include_prereleases" alt="Github Release" />
|
||||
</p>
|
||||
</p>
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -9,9 +22,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.
|
||||
|
||||
@@ -52,13 +71,15 @@ Advanced options allow you to define your own routines to handle controlling wha
|
||||
|
||||
### Typescript
|
||||
|
||||
This library gained Typescript support recently. You can install the types in the following way:
|
||||
```
|
||||
$ npm i -D @types/next-auth
|
||||
```
|
||||
In you encounter any issue with them, please raise an issue and add the "typescript" label to it, we'll try to help you with it as soon as possible.
|
||||
You can install the appropriate types via the following command:
|
||||
|
||||
Alternatively you can raise a PR directly with your fixes on [**DefinitelyTyped**](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/next-auth).
|
||||
```
|
||||
npm install --save-dev @types/next-auth
|
||||
```
|
||||
|
||||
If you encounter any problems with the types package, please create an issue and add the `typescript` label to it.
|
||||
|
||||
Alternatively, you can open a pull request directly with your fixes on the [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/next-auth) repository, where you'll find a `next-auth` subfolder.
|
||||
|
||||
## Example
|
||||
|
||||
@@ -68,7 +89,7 @@ Alternatively you can raise a PR directly with your fixes on [**DefinitelyTyped*
|
||||
import NextAuth from 'next-auth'
|
||||
import Providers from 'next-auth/providers'
|
||||
|
||||
const options = {
|
||||
export default NextAuth({
|
||||
providers: [
|
||||
// OAuth authentication providers
|
||||
Providers.Apple({
|
||||
@@ -87,9 +108,7 @@ 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
|
||||
@@ -118,14 +137,18 @@ export default function myComponent() {
|
||||
}
|
||||
```
|
||||
|
||||
## 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>
|
||||
|
||||
## 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/iaincollins/next-auth/blob/main/CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
ISC
|
||||
|
||||
6198
package-lock.json
generated
6198
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
21
package.json
21
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "next-auth",
|
||||
"version": "3.1.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",
|
||||
@@ -17,11 +17,12 @@
|
||||
"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",
|
||||
"test:db": "npm run test:db:mysql && npm run test:db:postgres && npm run test:db:mongodb && npm run test:db:mssql",
|
||||
"test:db": "npm run test:db:mysql && npm run test:db:postgres && npm run test:db:mongodb && npm run test:db:mssql && npm run test:db:fauna",
|
||||
"test:db:mysql": "node test/mysql.js",
|
||||
"test:db:postgres": "node test/postgres.js",
|
||||
"test:db:mongodb": "node test/mongodb.js",
|
||||
"test:db:mssql": "node test/mssql.js",
|
||||
"test:db:fauna": "node test/fauna.js",
|
||||
"test:integration": "mocha test/integration",
|
||||
"db:start": "docker-compose -f test/docker/databases.yml up -d",
|
||||
"db:stop": "docker-compose -f test/docker/databases.yml down",
|
||||
@@ -42,11 +43,12 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"crypto-js": "^4.0.0",
|
||||
"faunadb": "^3.0.1",
|
||||
"futoin-hkdf": "^1.3.2",
|
||||
"jose": "^1.27.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jwt-decode": "^2.2.0",
|
||||
"nodemailer": "^6.4.6",
|
||||
"nodemailer": "^6.4.16",
|
||||
"oauth": "^0.9.15",
|
||||
"preact": "^10.4.1",
|
||||
"preact-render-to-string": "^5.1.7",
|
||||
@@ -55,22 +57,27 @@
|
||||
"typeorm": "^0.2.24"
|
||||
},
|
||||
"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.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.8.4",
|
||||
"@babel/core": "^7.9.6",
|
||||
"@babel/preset-env": "^7.9.6",
|
||||
"@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",
|
||||
"autoprefixer": "^9.7.6",
|
||||
"babel-preset-preact": "^2.0.0",
|
||||
"conventional-changelog-conventionalcommits": "4.4.0",
|
||||
"cssnano": "^4.1.10",
|
||||
"dotenv": "^8.2.0",
|
||||
"mocha": "^8.1.3",
|
||||
@@ -83,7 +90,7 @@
|
||||
"puppeteer": "^5.2.1",
|
||||
"puppeteer-extra": "^3.1.15",
|
||||
"puppeteer-extra-plugin-stealth": "^2.6.1",
|
||||
"standard": "^14.3.3"
|
||||
"standard": "^16.0.3"
|
||||
},
|
||||
"standard": {
|
||||
"ignore": [
|
||||
|
||||
519
src/adapters/fauna/index.js
Normal file
519
src/adapters/fauna/index.js
Normal file
@@ -0,0 +1,519 @@
|
||||
import { query as q } from 'faunadb'
|
||||
import { createHash, randomBytes } from 'crypto'
|
||||
import logger from '../../lib/logger'
|
||||
|
||||
const Adapter = (config, options = {}) => {
|
||||
const {
|
||||
faunaClient,
|
||||
collections = {
|
||||
User: 'user',
|
||||
Account: 'account',
|
||||
Session: 'session',
|
||||
VerificationRequest: 'verification_request'
|
||||
},
|
||||
indexes = {
|
||||
Account: 'account_by_provider_account_id',
|
||||
User: 'user_by_email',
|
||||
Session: 'session_by_token',
|
||||
VerificationRequest: 'verification_request_by_token'
|
||||
}
|
||||
} = config
|
||||
|
||||
async function getAdapter (appOptions) {
|
||||
function _debug (debugCode, ...args) {
|
||||
logger.debug(`fauna_${debugCode}`, ...args)
|
||||
}
|
||||
|
||||
const defaultSessionMaxAge = 30 * 24 * 60 * 60 * 1000
|
||||
const sessionMaxAge = (appOptions && appOptions.session && appOptions.session.maxAge)
|
||||
? appOptions.session.maxAge * 1000
|
||||
: defaultSessionMaxAge
|
||||
const sessionUpdateAge = (appOptions && appOptions.session && appOptions.session.updateAge)
|
||||
? appOptions.session.updateAge * 1000
|
||||
: 0
|
||||
|
||||
async function createUser (profile) {
|
||||
_debug('createUser', profile)
|
||||
|
||||
const timestamp = new Date().toISOString()
|
||||
const FQL = q.Create(
|
||||
q.Collection(collections.User), {
|
||||
data: {
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: profile.image,
|
||||
emailVerified: profile.emailVerified
|
||||
? profile.emailVerified
|
||||
: false,
|
||||
createdAt: q.Time(timestamp),
|
||||
updatedAt: q.Time(timestamp)
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
const newUser = await faunaClient.query(FQL)
|
||||
newUser.data.id = newUser.ref.id
|
||||
|
||||
return newUser.data
|
||||
} catch (error) {
|
||||
console.error('CREATE_USER', error)
|
||||
return Promise.reject(new Error('CREATE_USER'))
|
||||
}
|
||||
}
|
||||
|
||||
async function getUser (id) {
|
||||
_debug('getUser', id)
|
||||
|
||||
const FQL = q.Get(
|
||||
q.Ref(q.Collection(collections.User), id)
|
||||
)
|
||||
|
||||
try {
|
||||
const user = await faunaClient.query(FQL)
|
||||
user.data.id = user.ref.id
|
||||
|
||||
return user.data
|
||||
} catch (error) {
|
||||
console.error('GET_USER', error)
|
||||
return Promise.reject(new Error('GET_USER'))
|
||||
}
|
||||
}
|
||||
|
||||
async function getUserByEmail (email) {
|
||||
_debug('getUserByEmail', email)
|
||||
|
||||
if (!email) {
|
||||
return null
|
||||
}
|
||||
|
||||
const FQL = q.Let(
|
||||
{
|
||||
ref: q.Match(q.Index(indexes.User), email)
|
||||
},
|
||||
q.If(
|
||||
q.Exists(q.Var('ref')),
|
||||
q.Get(q.Var('ref')),
|
||||
null
|
||||
)
|
||||
)
|
||||
|
||||
try {
|
||||
const user = await faunaClient.query(FQL)
|
||||
|
||||
if (user == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
user.data.id = user.ref.id
|
||||
return user.data
|
||||
} catch (error) {
|
||||
console.error('GET_USER_BY_EMAIL', error)
|
||||
return Promise.reject(new Error('GET_USER_BY_EMAIL'))
|
||||
}
|
||||
}
|
||||
|
||||
async function getUserByProviderAccountId (providerId, providerAccountId) {
|
||||
_debug('getUserByProviderAccountId', providerId, providerAccountId)
|
||||
|
||||
const FQL = q.Let(
|
||||
{
|
||||
ref: q.Match(
|
||||
q.Index(indexes.Account),
|
||||
[providerId, providerAccountId]
|
||||
)
|
||||
},
|
||||
q.If(
|
||||
q.Exists(q.Var('ref')),
|
||||
q.Get(
|
||||
q.Ref(
|
||||
q.Collection(collections.User),
|
||||
q.Select(['data', 'userId'],
|
||||
q.Get(q.Var('ref'))
|
||||
)
|
||||
)
|
||||
),
|
||||
null
|
||||
)
|
||||
)
|
||||
|
||||
try {
|
||||
const user = await faunaClient.query(FQL)
|
||||
|
||||
if (user == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
user.data.id = user.ref.id
|
||||
|
||||
return user.data
|
||||
} catch (error) {
|
||||
console.error('GET_USER_BY_PROVIDER_ACCOUNT_ID', error)
|
||||
return Promise.reject(new Error('GET_USER_BY_PROVIDER_ACCOUNT_ID'))
|
||||
}
|
||||
}
|
||||
|
||||
async function updateUser (user) {
|
||||
_debug('updateUser', user)
|
||||
|
||||
const timestamp = new Date().toISOString()
|
||||
const FQL = q.Update(
|
||||
q.Ref(q.Collection(collections.User), user.id),
|
||||
{
|
||||
data: {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
image: user.image,
|
||||
emailVerified: user.emailVerified ? user.emailVerified : false,
|
||||
updatedAt: q.Time(timestamp)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
try {
|
||||
const user = await faunaClient.query(FQL)
|
||||
user.data.id = user.ref.id
|
||||
|
||||
return user.data
|
||||
} catch (error) {
|
||||
console.error('UPDATE_USER_ERROR', error)
|
||||
return Promise.reject(new Error('UPDATE_USER_ERROR'))
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteUser (userId) {
|
||||
_debug('deleteUser', userId)
|
||||
|
||||
const FQL = q.Delete(
|
||||
q.Ref(q.Collection(collections.User), userId)
|
||||
)
|
||||
|
||||
try {
|
||||
await faunaClient.query(FQL)
|
||||
} catch (error) {
|
||||
console.error('DELETE_USER_ERROR', error)
|
||||
return Promise.reject(new Error('DELETE_USER_ERROR'))
|
||||
}
|
||||
}
|
||||
|
||||
async function linkAccount (userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires) {
|
||||
_debug('linkAccount', userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires)
|
||||
|
||||
try {
|
||||
const timestamp = new Date().toISOString()
|
||||
const account = await faunaClient.query(
|
||||
q.Create(q.Collection(collections.Account), {
|
||||
data: {
|
||||
userId: userId,
|
||||
providerId: providerId,
|
||||
providerType: providerType,
|
||||
providerAccountId: providerAccountId,
|
||||
refreshToken: refreshToken,
|
||||
accessToken: accessToken,
|
||||
accessTokenExpires: accessTokenExpires,
|
||||
createdAt: q.Time(timestamp),
|
||||
updatedAt: q.Time(timestamp)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return account.data
|
||||
} catch (error) {
|
||||
console.error('LINK_ACCOUNT_ERROR', error)
|
||||
return Promise.reject(new Error('LINK_ACCOUNT_ERROR'))
|
||||
}
|
||||
}
|
||||
|
||||
async function unlinkAccount (userId, providerId, providerAccountId) {
|
||||
_debug('unlinkAccount', userId, providerId, providerAccountId)
|
||||
|
||||
const FQL = q.Delete(
|
||||
q.Select('ref',
|
||||
q.Get(
|
||||
q.Match(
|
||||
q.Index(indexes.Account),
|
||||
[providerId, providerAccountId]
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
try {
|
||||
await faunaClient.query(FQL)
|
||||
} catch (error) {
|
||||
console.error('UNLINK_ACCOUNT_ERROR', error)
|
||||
return Promise.reject(new Error('UNLINK_ACCOUNT_ERROR'))
|
||||
}
|
||||
}
|
||||
|
||||
async function createSession (user) {
|
||||
_debug('createSession', user)
|
||||
|
||||
let expires = null
|
||||
if (sessionMaxAge) {
|
||||
const dateExpires = new Date()
|
||||
dateExpires.setTime(dateExpires.getTime() + sessionMaxAge)
|
||||
expires = dateExpires.toISOString()
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString()
|
||||
const FQL =
|
||||
q.Create(q.Collection(collections.Session), {
|
||||
data: {
|
||||
userId: user.id,
|
||||
expires: q.Time(expires),
|
||||
sessionToken: randomBytes(32).toString('hex'),
|
||||
accessToken: randomBytes(32).toString('hex'),
|
||||
createdAt: q.Time(timestamp),
|
||||
updatedAt: q.Time(timestamp)
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
const session = await faunaClient.query(FQL)
|
||||
|
||||
session.data.id = session.ref.id
|
||||
|
||||
return session.data
|
||||
} catch (error) {
|
||||
console.error('CREATE_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('CREATE_SESSION_ERROR'))
|
||||
}
|
||||
}
|
||||
|
||||
async function getSession (sessionToken) {
|
||||
_debug('getSession', sessionToken)
|
||||
|
||||
try {
|
||||
const session = await faunaClient.query(
|
||||
q.Get(
|
||||
q.Match(
|
||||
q.Index(indexes.Session),
|
||||
sessionToken
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// Check session has not expired (do not return it if it has)
|
||||
if (session && session.expires && new Date() > session.expires) {
|
||||
await _deleteSession(sessionToken)
|
||||
return null
|
||||
}
|
||||
|
||||
session.data.id = session.ref.id
|
||||
|
||||
return session.data
|
||||
} catch (error) {
|
||||
console.error('GET_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('GET_SESSION_ERROR'))
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSession (session, force) {
|
||||
_debug('updateSession', session)
|
||||
|
||||
try {
|
||||
const shouldUpdate = sessionMaxAge && (sessionUpdateAge || sessionUpdateAge === 0) && session.expires
|
||||
if (!shouldUpdate && !force) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Calculate last updated date, to throttle write updates to database
|
||||
// Formula: ({expiry date} - sessionMaxAge) + sessionUpdateAge
|
||||
// e.g. ({expiry date} - 30 days) + 1 hour
|
||||
//
|
||||
// Default for sessionMaxAge is 30 days.
|
||||
// Default for sessionUpdateAge is 1 hour.
|
||||
const dateSessionIsDueToBeUpdated = new Date(session.expires)
|
||||
dateSessionIsDueToBeUpdated.setTime(dateSessionIsDueToBeUpdated.getTime() - sessionMaxAge)
|
||||
dateSessionIsDueToBeUpdated.setTime(dateSessionIsDueToBeUpdated.getTime() + sessionUpdateAge)
|
||||
|
||||
// Trigger update of session expiry date and write to database, only
|
||||
// if the session was last updated more than {sessionUpdateAge} ago
|
||||
const currentDate = new Date()
|
||||
if (currentDate < dateSessionIsDueToBeUpdated && !force) {
|
||||
return null
|
||||
}
|
||||
|
||||
const newExpiryDate = new Date()
|
||||
newExpiryDate.setTime(newExpiryDate.getTime() + sessionMaxAge)
|
||||
|
||||
const updatedSession = await faunaClient.query(
|
||||
q.Update(
|
||||
q.Ref(q.Collection(collections.Session), session.id),
|
||||
{
|
||||
data: {
|
||||
expires: q.Time(newExpiryDate.toISOString()),
|
||||
updatedAt: q.Time(new Date().toISOString())
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
updatedSession.data.id = updatedSession.ref.id
|
||||
|
||||
return updatedSession.data
|
||||
} catch (error) {
|
||||
console.error('UPDATE_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('UPDATE_SESSION_ERROR'))
|
||||
}
|
||||
}
|
||||
|
||||
async function _deleteSession (sessionToken) {
|
||||
const FQL = q.Delete(
|
||||
q.Select('ref',
|
||||
q.Get(
|
||||
q.Match(
|
||||
q.Index(indexes.Session),
|
||||
sessionToken
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return faunaClient.query(FQL)
|
||||
}
|
||||
|
||||
async function deleteSession (sessionToken) {
|
||||
_debug('deleteSession', sessionToken)
|
||||
|
||||
try {
|
||||
return await _deleteSession(sessionToken)
|
||||
} catch (error) {
|
||||
console.error('DELETE_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('DELETE_SESSION_ERROR'))
|
||||
}
|
||||
}
|
||||
|
||||
async function createVerificationRequest (identifier, url, token, secret, provider) {
|
||||
_debug('createVerificationRequest', identifier)
|
||||
|
||||
const { baseUrl } = appOptions
|
||||
const { sendVerificationRequest, maxAge } = provider
|
||||
|
||||
// Store hashed token (using secret as salt) so that tokens cannot be exploited
|
||||
// even if the contents of the database is compromised
|
||||
// @TODO Use bcrypt function here instead of simple salted hash
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
|
||||
let expires = null
|
||||
if (maxAge) {
|
||||
const dateExpires = new Date()
|
||||
dateExpires.setTime(dateExpires.getTime() + (maxAge * 1000))
|
||||
|
||||
expires = dateExpires.toISOString()
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString()
|
||||
const FQL = q.Create(
|
||||
q.Collection(collections.VerificationRequest), {
|
||||
data: {
|
||||
identifier: identifier,
|
||||
token: hashedToken,
|
||||
expires: expires === null ? null : q.Time(expires),
|
||||
createdAt: q.Time(timestamp),
|
||||
updatedAt: q.Time(timestamp)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
try {
|
||||
const verificationRequest = await faunaClient.query(FQL)
|
||||
|
||||
// With the verificationCallback on a provider, you can send an email, or queue
|
||||
// an email to be sent, or perform some other action (e.g. send a text message)
|
||||
await sendVerificationRequest({ identifier, url, token, baseUrl, provider })
|
||||
|
||||
return verificationRequest.data
|
||||
} catch (error) {
|
||||
console.error('CREATE_VERIFICATION_REQUEST_ERROR', error)
|
||||
return Promise.reject(new Error('CREATE_VERIFICATION_REQUEST_ERROR'))
|
||||
}
|
||||
}
|
||||
|
||||
async function getVerificationRequest (identifier, token, secret, provider) {
|
||||
_debug('getVerificationRequest', identifier, token)
|
||||
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
const FQL = q.Let(
|
||||
{
|
||||
ref: q.Match(q.Index(indexes.VerificationRequest), hashedToken)
|
||||
},
|
||||
q.If(
|
||||
q.Exists(q.Var('ref')),
|
||||
{
|
||||
ref: q.Var('ref'),
|
||||
request: q.Select('data', q.Get(q.Var('ref')))
|
||||
},
|
||||
null
|
||||
)
|
||||
)
|
||||
|
||||
try {
|
||||
const { ref, request: verificationRequest } = await faunaClient.query(FQL)
|
||||
const nowDate = Date.now()
|
||||
|
||||
if (verificationRequest && verificationRequest.expires && verificationRequest.expires < nowDate) {
|
||||
// Delete the expired request so it cannot be used
|
||||
await faunaClient.query(
|
||||
q.Delete(ref)
|
||||
)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
return verificationRequest
|
||||
} catch (error) {
|
||||
console.error('GET_VERIFICATION_REQUEST_ERROR', error)
|
||||
return Promise.reject(new Error('GET_VERIFICATION_REQUEST_ERROR'))
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteVerificationRequest (identifier, token, secret, provider) {
|
||||
_debug('deleteVerification', identifier, token)
|
||||
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
const FQL = q.Delete(
|
||||
q.Select('ref',
|
||||
q.Get(
|
||||
q.Match(
|
||||
q.Index(indexes.VerificationRequest), hashedToken
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
try {
|
||||
await faunaClient.query(FQL)
|
||||
} catch (error) {
|
||||
console.error('DELETE_VERIFICATION_REQUEST_ERROR', error)
|
||||
return Promise.reject(new Error('DELETE_VERIFICATION_REQUEST_ERROR'))
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
createUser,
|
||||
getUser,
|
||||
getUserByEmail,
|
||||
getUserByProviderAccountId,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
linkAccount,
|
||||
unlinkAccount,
|
||||
createSession,
|
||||
getSession,
|
||||
updateSession,
|
||||
deleteSession,
|
||||
createVerificationRequest,
|
||||
getVerificationRequest,
|
||||
deleteVerificationRequest
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
getAdapter
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
Adapter
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import TypeORM from './typeorm'
|
||||
import Prisma from './prisma'
|
||||
import Fauna from './fauna'
|
||||
|
||||
export default {
|
||||
Default: TypeORM.Adapter,
|
||||
TypeORM,
|
||||
Prisma
|
||||
Prisma,
|
||||
Fauna
|
||||
}
|
||||
|
||||
@@ -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,9 +78,9 @@ const Adapter = (config) => {
|
||||
async function getUserByProviderAccountId (providerId, providerAccountId) {
|
||||
debug('GET_USER_BY_PROVIDER_ACCOUNT_ID', providerId, providerAccountId)
|
||||
try {
|
||||
const account = await 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].findOne({ where: { id: account.userId } })
|
||||
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))
|
||||
@@ -174,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) {
|
||||
@@ -280,7 +280,7 @@ 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].findUnique({ where: { token: hashedToken } })
|
||||
|
||||
if (verificationRequest && verificationRequest.expires && new Date() > verificationRequest.expires) {
|
||||
// Delete verification entry so it cannot be used again
|
||||
|
||||
24
src/providers/azure-ad-b2c.js
Normal file
24
src/providers/azure-ad-b2c.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export default (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
|
||||
}
|
||||
}
|
||||
44
src/providers/bungie.js
Normal file
44
src/providers/bungie.js
Normal file
@@ -0,0 +1,44 @@
|
||||
export default (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/',
|
||||
prepareProfileRequest: ({ provider, url, headers, results }) => {
|
||||
if (!results.membership_id) {
|
||||
// internal error
|
||||
// @TODO: handle better
|
||||
throw new Error('Expected membership_id to be passed.')
|
||||
}
|
||||
|
||||
if (!provider.apiKey) {
|
||||
throw new Error('The Bungie provider requires the apiKey option to be present.')
|
||||
}
|
||||
|
||||
headers['X-API-Key'] = provider.apiKey
|
||||
url = url.replace('{membershipId}', results.membership_id)
|
||||
|
||||
return url
|
||||
},
|
||||
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
|
||||
}
|
||||
},
|
||||
apiKey: null,
|
||||
clientId: null,
|
||||
clientSecret: null,
|
||||
...options
|
||||
}
|
||||
}
|
||||
@@ -7,14 +7,20 @@ export default (options) => {
|
||||
scope: 'identify email',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: 'https://discord.com/api/oauth2/token',
|
||||
authorizationUrl:
|
||||
'https://discord.com/api/oauth2/authorize?response_type=code&prompt=none',
|
||||
authorizationUrl: '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.premium_type === 1 || profile.premium_type === 2 ? '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`,
|
||||
image: profile.image_url,
|
||||
email: profile.email
|
||||
}
|
||||
},
|
||||
|
||||
22
src/providers/foursquare.js
Normal file
22
src/providers/foursquare.js
Normal file
@@ -0,0 +1,22 @@
|
||||
export default ({ 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
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
import Apple from './apple'
|
||||
import Atlassian from './atlassian'
|
||||
import Auth0 from './auth0'
|
||||
import AzureADB2C from './azure-ad-b2c'
|
||||
import Basecamp from './basecamp'
|
||||
import BattleNet from './battlenet'
|
||||
import Box from './box'
|
||||
import Bungie from './bungie'
|
||||
import Credentials from './credentials'
|
||||
import Cognito from './cognito'
|
||||
import Discord from './discord'
|
||||
import Email from './email'
|
||||
import Facebook from './facebook'
|
||||
import Foursquare from './foursquare'
|
||||
import FusionAuth from './fusionauth'
|
||||
import GitHub from './github'
|
||||
import GitLab from './gitlab'
|
||||
@@ -16,9 +19,11 @@ import Google from './google'
|
||||
import IdentityServer4 from './identity-server4'
|
||||
import LinkedIn from './linkedin'
|
||||
import Mixer from './mixer'
|
||||
import Netlify from './netlify'
|
||||
import Okta from './okta'
|
||||
import Slack from './slack'
|
||||
import Spotify from './spotify'
|
||||
import Strava from './strava'
|
||||
import Twitch from './twitch'
|
||||
import Twitter from './twitter'
|
||||
import Yandex from './yandex'
|
||||
@@ -27,14 +32,17 @@ export default {
|
||||
Atlassian,
|
||||
Auth0,
|
||||
Apple,
|
||||
AzureADB2C,
|
||||
Basecamp,
|
||||
BattleNet,
|
||||
Box,
|
||||
Bungie,
|
||||
Credentials,
|
||||
Cognito,
|
||||
Discord,
|
||||
Email,
|
||||
Facebook,
|
||||
Foursquare,
|
||||
FusionAuth,
|
||||
GitHub,
|
||||
GitLab,
|
||||
@@ -42,9 +50,11 @@ export default {
|
||||
IdentityServer4,
|
||||
LinkedIn,
|
||||
Mixer,
|
||||
Netlify,
|
||||
Okta,
|
||||
Slack,
|
||||
Spotify,
|
||||
Strava,
|
||||
Twitter,
|
||||
Twitch,
|
||||
Yandex
|
||||
|
||||
21
src/providers/netlify.js
Normal file
21
src/providers/netlify.js
Normal file
@@ -0,0 +1,21 @@
|
||||
export default (options) => {
|
||||
return {
|
||||
id: 'netlify',
|
||||
name: 'Netlify',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: 'https://api.netlify.com/oauth/token',
|
||||
authorizationUrl: 'https://app.netlify.com/authorize?response_type=code',
|
||||
profileUrl: 'https://api.netlify.com/api/v1/user',
|
||||
profile: (profile) => {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.full_name,
|
||||
email: profile.email,
|
||||
image: profile.avatar_url
|
||||
}
|
||||
},
|
||||
...options
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,9 @@ export default (options) => {
|
||||
client_secret: options.clientSecret
|
||||
},
|
||||
// These will be different depending on the Org.
|
||||
accessTokenUrl: `https://${options.domain}/oauth2/v1/token`,
|
||||
authorizationUrl: `https://${options.domain}/oauth2/v1/authorize/?response_type=code`,
|
||||
profileUrl: `https://${options.domain}/oauth2/v1/userinfo/`,
|
||||
accessTokenUrl: `https://${options.domain}/v1/token`,
|
||||
authorizationUrl: `https://${options.domain}/v1/authorize/?response_type=code`,
|
||||
profileUrl: `https://${options.domain}/v1/userinfo/`,
|
||||
profile: (profile) => {
|
||||
return { ...profile, id: profile.sub }
|
||||
},
|
||||
|
||||
@@ -4,10 +4,12 @@ export default (options) => {
|
||||
name: 'Slack',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: 'identity.basic identity.email identity.avatar',
|
||||
scope: [],
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: 'https://slack.com/api/oauth.access',
|
||||
authorizationUrl: 'https://slack.com/oauth/authorize?response_type=code',
|
||||
accessTokenUrl: 'https://slack.com/api/oauth.v2.access',
|
||||
accessTokenGetter: (json) => json.authed_user.access_token,
|
||||
authorizationUrl: 'https://slack.com/oauth/v2/authorize',
|
||||
additionalAuthorizeParams: { user_scope: 'identity.basic,identity.email,identity.avatar' },
|
||||
profileUrl: 'https://slack.com/api/users.identity',
|
||||
profile: (profile) => {
|
||||
const { user } = profile
|
||||
|
||||
@@ -15,7 +15,7 @@ export default (options) => {
|
||||
id: profile.id,
|
||||
name: profile.display_name,
|
||||
email: profile.email,
|
||||
image: profile.images.length > 0 ? profile.images[0].url : undefined
|
||||
image: profile.images?.[0]?.url
|
||||
}
|
||||
},
|
||||
...options
|
||||
|
||||
22
src/providers/strava.js
Normal file
22
src/providers/strava.js
Normal file
@@ -0,0 +1,22 @@
|
||||
export default (options) => {
|
||||
return {
|
||||
id: 'strava',
|
||||
name: 'Strava',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: 'read',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: 'https://www.strava.com/api/v3/oauth/token',
|
||||
authorizationUrl:
|
||||
'https://www.strava.com/api/v3/oauth/authorize?response_type=code',
|
||||
profileUrl: 'https://www.strava.com/api/v3/athlete',
|
||||
profile: (profile) => {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.firstname,
|
||||
image: profile.profile
|
||||
}
|
||||
},
|
||||
...options
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ if (!process.env.NEXTAUTH_URL) {
|
||||
logger.warn('NEXTAUTH_URL', 'NEXTAUTH_URL environment variable not set')
|
||||
}
|
||||
|
||||
export default async (req, res, userSuppliedOptions) => {
|
||||
async function NextAuth (req, res, userSuppliedOptions) {
|
||||
// To the best of my knowledge, we need to return a promise here
|
||||
// to avoid early termination of calls to the serverless function
|
||||
// (and then return that promise when we are done) - eslint
|
||||
@@ -32,6 +32,18 @@ export default async (req, res, userSuppliedOptions) => {
|
||||
// safe to return and that no more data will be sent.
|
||||
const done = resolve
|
||||
|
||||
if (!req.query.nextauth) {
|
||||
const error = 'Cannot find [...nextauth].js in pages/api/auth. Make sure the filename is written correctly.'
|
||||
|
||||
logger.error('MISSING_NEXTAUTH_API_ROUTE_ERROR', error)
|
||||
res
|
||||
.status(500)
|
||||
.end(
|
||||
`Error: ${error}`
|
||||
)
|
||||
return done()
|
||||
}
|
||||
|
||||
const { url, query, body } = req
|
||||
const {
|
||||
nextauth,
|
||||
@@ -313,3 +325,11 @@ export default async (req, res, userSuppliedOptions) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default async (...args) => {
|
||||
if (args.length === 1) {
|
||||
return (req, res) => NextAuth(req, res, args[0])
|
||||
}
|
||||
|
||||
return NextAuth(...args)
|
||||
}
|
||||
|
||||
@@ -52,8 +52,8 @@ export default async (sessionToken, profile, providerAccount, options) => {
|
||||
if (useJwtSession) {
|
||||
try {
|
||||
session = await jwt.decode({ ...jwt, token: sessionToken })
|
||||
if (session && session.user) {
|
||||
user = await getUser(session.user.id)
|
||||
if (session && session.sub) {
|
||||
user = await getUser(session.sub)
|
||||
isSignedIn = !!user
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -93,6 +93,7 @@ export default async (req, provider, csrfToken, callback) => {
|
||||
client.get(
|
||||
provider,
|
||||
accessToken,
|
||||
results,
|
||||
async (error, profileData) => {
|
||||
const { profile, account, OAuthProfile } = await _getProfile(error, profileData, accessToken, refreshToken, provider)
|
||||
callback(error, profile, account, OAuthProfile)
|
||||
@@ -210,10 +211,12 @@ async function _getOAuthAccessToken (code, provider, callback) {
|
||||
if (!params.redirect_uri) { params.redirect_uri = provider.callbackUrl }
|
||||
|
||||
if (!headers['Content-Type']) { headers['Content-Type'] = 'application/x-www-form-urlencoded' }
|
||||
|
||||
// Added as a fix to accomodate change in Twitch oAuth API
|
||||
if (!headers['Client-ID']) { headers['Client-ID'] = provider.clientId }
|
||||
|
||||
// Added as a fix for Reddit Authentication
|
||||
if (provider.id === 'reddit') {
|
||||
headers.Authorization = 'Basic ' + Buffer.from((provider.clientId + ':' + provider.clientSecret)).toString('base64')
|
||||
}
|
||||
// Okta errors when this is set. Maybe there are other Providers that also wont like this.
|
||||
if (setGetAccessTokenAuthHeader) {
|
||||
if (!headers.Authorization) { headers.Authorization = `Bearer ${code}` }
|
||||
@@ -244,16 +247,21 @@ async function _getOAuthAccessToken (code, provider, callback) {
|
||||
// Clients of these services suffer a minor performance cost.
|
||||
results = querystring.parse(data)
|
||||
}
|
||||
const accessToken = results.access_token
|
||||
const accessToken = provider.accessTokenGetter ? provider.accessTokenGetter(results) : results.access_token
|
||||
const refreshToken = results.refresh_token
|
||||
callback(null, accessToken, refreshToken, results)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Ported from https://github.com/ciaranj/node-oauth/blob/a7f8a1e21c362eb4ed2039431fb9ac2ae749f26a/lib/oauth2.js
|
||||
function _get (provider, accessToken, callback) {
|
||||
const url = provider.profileUrl
|
||||
/**
|
||||
* Ported from https://github.com/ciaranj/node-oauth/blob/a7f8a1e21c362eb4ed2039431fb9ac2ae749f26a/lib/oauth2.js
|
||||
*
|
||||
* 18/08/2020 @robertcraigie added results parameter to pass data to an optional request preparer.
|
||||
* e.g. see providers/bungie
|
||||
*/
|
||||
function _get (provider, accessToken, results, callback) {
|
||||
let url = provider.profileUrl
|
||||
const headers = provider.headers || {}
|
||||
|
||||
if (this._useAuthorizationHeaderForGET) {
|
||||
@@ -264,6 +272,11 @@ function _get (provider, accessToken, callback) {
|
||||
accessToken = null
|
||||
}
|
||||
|
||||
const prepareRequest = provider.prepareProfileRequest
|
||||
if (prepareRequest) {
|
||||
url = prepareRequest({ provider, url, headers, results }) || url
|
||||
}
|
||||
|
||||
this._request('GET', url, headers, null, accessToken, callback)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ export default (provider, csrfToken, callback, authParams) => {
|
||||
redirect_uri: provider.callbackUrl,
|
||||
scope: provider.scope,
|
||||
// A hash of the NextAuth.js CSRF token is used as the state
|
||||
state: createHash('sha256').update(csrfToken).digest('hex')
|
||||
state: createHash('sha256').update(csrfToken).digest('hex'),
|
||||
...provider.additionalAuthorizeParams
|
||||
})
|
||||
|
||||
// If the authorizationUrl specified in the config has query parameters on it
|
||||
|
||||
@@ -25,31 +25,33 @@ export default ({ baseUrl, basePath, error, res }) => {
|
||||
case 'Configuration':
|
||||
statusCode = 500
|
||||
heading = <h1>Server error</h1>
|
||||
message =
|
||||
message = (
|
||||
<div>
|
||||
<div className='message'>
|
||||
<p>There is a problem with the server configuration.</p>
|
||||
<p>Check the server logs for more information.</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
break
|
||||
case 'AccessDenied':
|
||||
statusCode = 403
|
||||
heading = <h1>Access Denied</h1>
|
||||
message =
|
||||
message = (
|
||||
<div>
|
||||
<div className='message'>
|
||||
<p>You do not have permission to sign in.</p>
|
||||
<p><a className='button' href={signinPageUrl}>Sign in</a></p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
break
|
||||
case 'Verification':
|
||||
// @TODO Check if user is signed in already with the same email address.
|
||||
// If they are, no need to display this message, can just direct to callbackUrl
|
||||
statusCode = 403
|
||||
heading = <h1>Unable to sign in</h1>
|
||||
message =
|
||||
message = (
|
||||
<div>
|
||||
<div className='message'>
|
||||
<p>The sign in link is no longer valid.</p>
|
||||
@@ -57,6 +59,7 @@ export default ({ baseUrl, basePath, error, res }) => {
|
||||
</div>
|
||||
<p><a className='button' href={signinPageUrl}>Sign in</a></p>
|
||||
</div>
|
||||
)
|
||||
break
|
||||
default:
|
||||
}
|
||||
|
||||
@@ -87,7 +87,8 @@ export default async (req, res, options, done) => {
|
||||
const defaultJwtPayload = {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
picture: user.image
|
||||
picture: user.image,
|
||||
sub: user.id?.toString()
|
||||
}
|
||||
const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, OAuthProfile, isNewUser)
|
||||
|
||||
@@ -110,7 +111,7 @@ export default async (req, res, options, done) => {
|
||||
// e.g. option to send users to a new account landing page on initial login
|
||||
// Note that the callback URL is preserved, so the journey can still be resumed
|
||||
if (isNewUser && pages.newUser) {
|
||||
return redirect(pages.newUser)
|
||||
return redirect(`${pages.newUser}${pages.newUser.includes('?') ? '&' : '?'}callbackUrl=${encodeURIComponent(callbackUrl)}`)
|
||||
}
|
||||
|
||||
// Callback URL is already verified at this point, so safe to use if specified
|
||||
@@ -177,7 +178,8 @@ export default async (req, res, options, done) => {
|
||||
const defaultJwtPayload = {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
picture: user.image
|
||||
picture: user.image,
|
||||
sub: user.id?.toString()
|
||||
}
|
||||
const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, profile, isNewUser)
|
||||
|
||||
@@ -200,7 +202,7 @@ export default async (req, res, options, done) => {
|
||||
// e.g. option to send users to a new account landing page on initial login
|
||||
// Note that the callback URL is preserved, so the journey can still be resumed
|
||||
if (isNewUser && pages.newUser) {
|
||||
return redirect(pages.newUser)
|
||||
return redirect(`${pages.newUser}${pages.newUser.includes('?') ? '&' : '?'}callbackUrl=${encodeURIComponent(callbackUrl)}`)
|
||||
}
|
||||
|
||||
// Callback URL is already verified at this point, so safe to use if specified
|
||||
|
||||
@@ -5,7 +5,7 @@ export default (req, res, options, done) => {
|
||||
const { providers } = options
|
||||
|
||||
const result = {}
|
||||
Object.entries(providers).map(([provider, providerConfig]) => {
|
||||
Object.entries(providers).forEach(([provider, providerConfig]) => {
|
||||
result[provider] = {
|
||||
id: provider,
|
||||
name: providerConfig.name,
|
||||
|
||||
1430
test/docker/app/package-lock.json
generated
1430
test/docker/app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@
|
||||
"author": "Iain Collins <me@iaincollins.com>",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"next": "^9.5.0",
|
||||
"next": "^9.5.4",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1"
|
||||
}
|
||||
|
||||
@@ -34,3 +34,10 @@ services:
|
||||
service: postgres
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
fauna:
|
||||
extends:
|
||||
file: databases/fauna.yml
|
||||
service: fauna
|
||||
ports:
|
||||
- 8443:8443
|
||||
|
||||
7
test/docker/databases/fauna.yml
Normal file
7
test/docker/databases/fauna.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
|
||||
fauna:
|
||||
image: fauna/faunadb
|
||||
restart: always
|
||||
200
test/fauna.js
Normal file
200
test/fauna.js
Normal file
@@ -0,0 +1,200 @@
|
||||
/* eslint-disable */
|
||||
const Adapters = require('../adapters');
|
||||
const assert = require('assert');
|
||||
const fauna = require('faunadb');
|
||||
const q = fauna.query;
|
||||
|
||||
const adminClient = new fauna.Client({
|
||||
secret: 'secret',
|
||||
domain: 'localhost',
|
||||
port: '8443',
|
||||
scheme: 'http'
|
||||
});
|
||||
|
||||
// Authenticated client against the new DB used for tests
|
||||
let client = null;
|
||||
|
||||
const InitialiseDb = async () => {
|
||||
await adminClient.query(
|
||||
q.CreateDatabase({name: 'nextauth'})
|
||||
);
|
||||
|
||||
const key = await adminClient.query(
|
||||
q.CreateKey({
|
||||
database: q.Database('nextauth'),
|
||||
role: 'server'
|
||||
})
|
||||
);
|
||||
|
||||
client = new fauna.Client({
|
||||
secret: key.secret,
|
||||
domain: 'localhost',
|
||||
port: '8443',
|
||||
scheme: 'http'
|
||||
});
|
||||
|
||||
await client.query(q.CreateCollection({name: 'account'}));
|
||||
await client.query(q.CreateCollection({name: 'session'}));
|
||||
await client.query(q.CreateCollection({name: 'user'}));
|
||||
await client.query(q.CreateCollection({name: 'verification_request'}));
|
||||
|
||||
await client.query(q.CreateIndex({
|
||||
name: 'account_by_provider_account_id',
|
||||
source: q.Collection('account'),
|
||||
unique: true,
|
||||
terms: [
|
||||
{ field: ['data', 'providerId'] },
|
||||
{ field: ['data', 'providerAccountId'] }
|
||||
]
|
||||
}));
|
||||
|
||||
await client.query(q.CreateIndex({
|
||||
name: 'session_by_token',
|
||||
source: q.Collection('session'),
|
||||
unique: true,
|
||||
terms: [
|
||||
{ field: ['data', 'sessionToken'] }
|
||||
]
|
||||
}));
|
||||
|
||||
await client.query(q.CreateIndex({
|
||||
name: 'user_by_email',
|
||||
source: q.Collection('user'),
|
||||
unique: true,
|
||||
terms: [
|
||||
{ field: ['data', 'email'] }
|
||||
]
|
||||
}));
|
||||
|
||||
await client.query(q.CreateIndex({
|
||||
name: 'verification_request_by_token',
|
||||
source: q.Collection('verification_request'),
|
||||
unique: true,
|
||||
terms: [
|
||||
{ field: ['data', 'token'] }
|
||||
]
|
||||
}));
|
||||
}
|
||||
|
||||
const RunTests = async (adapter) => {
|
||||
// createUser
|
||||
const newUserResult = await adapter.createUser({
|
||||
name: 'test user',
|
||||
email: 'user@name.test',
|
||||
image: 'https://www.gravatar.com/avatar/0'
|
||||
});
|
||||
|
||||
assert.strictEqual(newUserResult.name, 'test user');
|
||||
assert(newUserResult.createdAt !== null);
|
||||
|
||||
const userId = newUserResult.id;
|
||||
|
||||
// getUser
|
||||
const user = await adapter.getUser(newUserResult.id);
|
||||
assert.strictEqual(user.id, userId);
|
||||
|
||||
// getUserByEmail
|
||||
const userByEmaiil = await adapter.getUserByEmail('user@name.test');
|
||||
assert.strictEqual(userByEmaiil.id, userId);
|
||||
|
||||
// updateUser
|
||||
const update = {
|
||||
...user,
|
||||
name: 'updated name'
|
||||
};
|
||||
const updatedUser = await adapter.updateUser(update);
|
||||
assert.strictEqual(updatedUser.name, 'updated name');
|
||||
assert.strictEqual(updatedUser.id, userId);
|
||||
|
||||
// linkAccount
|
||||
const account = await adapter.linkAccount(
|
||||
userId,
|
||||
'github',
|
||||
'oauth',
|
||||
756832,
|
||||
undefined,
|
||||
'b7e3b00f2c596abc445f11abc445f1104c1b2b',
|
||||
null
|
||||
);
|
||||
assert.strictEqual(account.userId, userId);
|
||||
assert.strictEqual(account.providerId, 'github');
|
||||
assert(account.createdAt !== null);
|
||||
|
||||
// getUserByProviderAccountId
|
||||
const userByProviderAccountId = await adapter.getUserByProviderAccountId('github', 756832);
|
||||
assert.strictEqual(userByProviderAccountId.email, user.email);
|
||||
|
||||
// createSession
|
||||
const newSession = await adapter.createSession(user);
|
||||
assert(newSession.sessionToken !== null);
|
||||
assert(newSession.createdAt !== null);
|
||||
assert(newSession.expires !== null);
|
||||
|
||||
// getSession
|
||||
const session = await adapter.getSession(newSession.sessionToken);
|
||||
assert.strictEqual(session.sessionToken, newSession.sessionToken);
|
||||
|
||||
// updateSession
|
||||
const updatedSession = await adapter.updateSession(session);
|
||||
assert(updatedSession.expires !== session.expires);
|
||||
|
||||
// deleteSession
|
||||
await adapter.deleteSession(session.sessionToken);
|
||||
|
||||
// unlinkAccount
|
||||
await adapter.unlinkAccount(userId, 'github', 756832);
|
||||
|
||||
// deleteUser
|
||||
await adapter.deleteUser(userId);
|
||||
|
||||
// createVerificationRequest
|
||||
let requestSent = false;
|
||||
const newVerificationRequest = await adapter.createVerificationRequest(
|
||||
'user@test.test',
|
||||
'http://localhost/callback/email?email=test@test.test&token=123',
|
||||
'123',
|
||||
'abc',
|
||||
{
|
||||
sendVerificationRequest: ({}) => {
|
||||
requestSent = true;
|
||||
}
|
||||
}
|
||||
);
|
||||
assert.strictEqual(newVerificationRequest.identifier, 'user@test.test');
|
||||
assert(newVerificationRequest.token !== null && newVerificationRequest.token !== '');
|
||||
assert(requestSent === true);
|
||||
|
||||
// getVerificationRequest
|
||||
const verificationRequest = await adapter.getVerificationRequest('user@test.test', '123', 'abc');
|
||||
assert.strictEqual(verificationRequest.identifier, 'user@test.test');
|
||||
assert.strictEqual(verificationRequest.token, newVerificationRequest.token);
|
||||
|
||||
// deleteVerificationRequest
|
||||
await adapter.deleteVerificationRequest('user@test.test', '123', 'abc');
|
||||
}
|
||||
|
||||
;(async () => {
|
||||
let error = false;
|
||||
|
||||
try {
|
||||
// Initialise collections and create indexes
|
||||
await InitialiseDb();
|
||||
|
||||
const adapterFactory = Adapters.Fauna.Adapter({faunaClient: client});
|
||||
const adapter = await adapterFactory.getAdapter({baseUrl: 'http://localhost'});
|
||||
|
||||
await RunTests(adapter);
|
||||
console.log('FaunaDB loaded ok');
|
||||
} catch (error) {
|
||||
console.error('FaunaDB error', error);
|
||||
error = true;
|
||||
} finally {
|
||||
// Clean up the DB
|
||||
await adminClient.query(
|
||||
q.Delete(q.Database('nextauth'))
|
||||
);
|
||||
}
|
||||
|
||||
const retCode = error ? 1 : 0;
|
||||
process.exit(retCode);
|
||||
})();
|
||||
@@ -17,16 +17,16 @@ You can specify a handler for any of the callbacks below.
|
||||
...
|
||||
callbacks: {
|
||||
signIn: async (user, account, profile) => {
|
||||
return Promise.resolve(true)
|
||||
return true
|
||||
},
|
||||
redirect: async (url, baseUrl) => {
|
||||
return Promise.resolve(baseUrl)
|
||||
return baseUrl
|
||||
},
|
||||
session: async (session, user) => {
|
||||
return Promise.resolve(session)
|
||||
return session
|
||||
},
|
||||
jwt: async (token, user, account, profile, isNewUser) => {
|
||||
return Promise.resolve(token)
|
||||
return token
|
||||
}
|
||||
...
|
||||
}
|
||||
@@ -50,13 +50,13 @@ callbacks: {
|
||||
signIn: async (user, account, profile) => {
|
||||
const isAllowedToSignIn = true
|
||||
if (isAllowedToSignIn) {
|
||||
return Promise.resolve(true)
|
||||
return true
|
||||
} else {
|
||||
// Return false to display a default error message
|
||||
return Promise.resolve(false)
|
||||
return false
|
||||
// You can also Reject this callback with an Error or with a URL:
|
||||
// return Promise.reject(new Error('error message')) // Redirect to error page
|
||||
// return Promise.reject('/path/to/redirect') // Redirect to a URL
|
||||
// throw new Error('error message') // Redirect to error page
|
||||
// return '/path/to/redirect' // Redirect to a URL
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,8 +97,8 @@ callbacks: {
|
||||
*/
|
||||
redirect: async (url, baseUrl) => {
|
||||
return url.startsWith(baseUrl)
|
||||
? Promise.resolve(url)
|
||||
: Promise.resolve(baseUrl)
|
||||
? url
|
||||
: baseUrl
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -127,7 +127,7 @@ callbacks: {
|
||||
*/
|
||||
session: async (session, user) => {
|
||||
session.foo = 'bar' // Add property to session
|
||||
return Promise.resolve(session)
|
||||
return session
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -148,7 +148,7 @@ If using JSON Web Tokens instead of database sessions, you should use the User I
|
||||
## JWT callback
|
||||
|
||||
This JSON Web Token callback is called whenever a JSON Web Token is created (i.e. at sign
|
||||
in) or updated (i.e whenever a session is accesed in the client).
|
||||
in) or updated (i.e whenever a session is accessed in the client).
|
||||
|
||||
e.g. `/api/auth/signin`, `getSession()`, `useSession()`, `/api/auth/session`
|
||||
|
||||
@@ -171,7 +171,7 @@ callbacks: {
|
||||
const isSignIn = (user) ? true : false
|
||||
// Add auth_time to token on signin in
|
||||
if (isSignIn) { token.auth_time = Math.floor(Date.now() / 1000) }
|
||||
return Promise.resolve(token)
|
||||
return token
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -144,7 +144,7 @@ Install module:
|
||||
#### Example
|
||||
|
||||
```js
|
||||
database: 'postgres://username:password@127.0.0.1:3306/database_name'
|
||||
database: 'postgres://username:password@127.0.0.1:5432/database_name'
|
||||
```
|
||||
|
||||
### Microsoft SQL Server
|
||||
@@ -166,7 +166,7 @@ Install module:
|
||||
#### Example
|
||||
|
||||
```js
|
||||
database: 'mongodb://username:password@127.0.0.1:3306/database_name'
|
||||
database: 'mongodb://username:password@127.0.0.1:27017/database_name'
|
||||
```
|
||||
|
||||
### SQLite
|
||||
|
||||
@@ -231,16 +231,16 @@ You can specify a handler for any of the callbacks below.
|
||||
```js
|
||||
callbacks: {
|
||||
signIn: async (user, account, profile) => {
|
||||
return Promise.resolve(true)
|
||||
return true
|
||||
},
|
||||
redirect: async (url, baseUrl) => {
|
||||
return Promise.resolve(baseUrl)
|
||||
return baseUrl
|
||||
},
|
||||
session: async (session, user) => {
|
||||
return Promise.resolve(session)
|
||||
return session
|
||||
},
|
||||
jwt: async (token, user, account, profile, isNewUser) => {
|
||||
return Promise.resolve(token)
|
||||
return token
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -277,7 +277,7 @@ events: {
|
||||
|
||||
### adapter
|
||||
|
||||
* **Default value**: *Adapater.Default()*
|
||||
* **Default value**: *Adapter.Default()*
|
||||
* **Required**: *No*
|
||||
|
||||
#### Description
|
||||
|
||||
@@ -14,12 +14,14 @@ NextAuth.js is designed to work with any OAuth service, it supports OAuth 1.0, 1
|
||||
* [Apple](/providers/apple)
|
||||
* [Atlassian](/providers/atlassian)
|
||||
* [Auth0](/providers/auth0)
|
||||
* [Azure Active Directory B2C](/providers/azure-ad-b2c)
|
||||
* [Basecamp](/providers/basecamp)
|
||||
* [Battle.net](/providers/battlenet)
|
||||
* [Battle.net](/providers/battle.net)
|
||||
* [Box](/providers/box)
|
||||
* [Amazon Cognito](/providers/cognito)
|
||||
* [Discord](/providers/discord)
|
||||
* [Facebook](/providers/facebook)
|
||||
* [Foursquare](/providers/foursquare)
|
||||
* [FusionAuth](/providers/fusionauth)
|
||||
* [GitHub](/providers/github)
|
||||
* [GitLab](/providers/gitlab)
|
||||
@@ -27,9 +29,11 @@ NextAuth.js is designed to work with any OAuth service, it supports OAuth 1.0, 1
|
||||
* [IdentityServer4](/providers/identity-server4)
|
||||
* [LinkedIn](/providers/LinkedIn)
|
||||
* [Mixer](/providers/Mixer)
|
||||
* [Netlify](/providers/Netlify)
|
||||
* [Okta](/providers/Okta)
|
||||
* [Slack](/providers/slack)
|
||||
* [Spotify](/providers/spotify)
|
||||
* [Strava](/providers/strava)
|
||||
* [Twitch](/providers/Twitch)
|
||||
* [Twitter](/providers/twitter)
|
||||
* [Yandex](/providers/yandex)
|
||||
@@ -103,7 +107,7 @@ As an example of what this looks like, this is the the provider object returned
|
||||
clientSecret: ''
|
||||
}
|
||||
```
|
||||
You can replace all the options in this JSON object with the ones from your custom provider – be sure to give it a unique ID and specify the correct OAuth version - and add it to the providers option:
|
||||
You can replace all the options in this JSON object with the ones from your custom provider – be sure to give it a unique ID and specify the correct OAuth version - and add it to the providers option:
|
||||
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
...
|
||||
@@ -135,6 +139,7 @@ providers: [
|
||||
| scope | OAuth access scopes (expects array or string) | No |
|
||||
| params | Additional authorization URL parameters | No |
|
||||
| accessTokenUrl | Endpoint to retrieve an access token | Yes |
|
||||
| accessTokenGetter | Default `(json) => json.access_token` | No |
|
||||
| requestTokenUrl | Endpoint to retrieve a request token | No |
|
||||
| authorizationUrl | Endpoint to request authorization from the user | Yes |
|
||||
| profileUrl | Endpoint to retrieve the user's profile | No |
|
||||
@@ -203,9 +208,9 @@ providers: [
|
||||
}
|
||||
if (user) {
|
||||
// Any user object returned here will be saved in the JSON Web Token
|
||||
return Promise.resolve(user)
|
||||
return user
|
||||
} else {
|
||||
return Promise.resolve(null)
|
||||
return null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -12,6 +12,7 @@ title: Contributors
|
||||
* <a href="https://github.com/geraldnolan">Gerald Nolan</a>
|
||||
* <a href="https://github.com/lluia">Lluis Agusti</a>
|
||||
* <a href="https://github.com/JeffersonBledsoe">Jefferson Bledsoe</a>
|
||||
* <a href="https://github.com/balazsorban44">Balázs Orbán</a>
|
||||
|
||||
_Special thanks to Lori Karikari for creating most of the providers, to Nico Domino for creating this site, to Fredrik Pettersen for creating the Prisma adapter, to Gerald Nolan for adding support for Sign in with Apple, to Lluis Agusti for work to add TypeScript definitions and to Jefferson Bledsoe for working on automating testing._
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ If you are using a Credentials Provider, NextAuth.js will not persist users or s
|
||||
In _most cases_ it does not make sense to specify a database in NextAuth.js options and support a Credentials Provider.
|
||||
|
||||
#### CALLBACK_CREDENTIALS_HANDLER_ERROR
|
||||
|
||||
---
|
||||
|
||||
### Session Handling
|
||||
@@ -127,3 +128,9 @@ They all indicate a problem interacting with the database.
|
||||
This error occurs when the Email Authentication Provider is unable to send an email.
|
||||
|
||||
Check your mail server configuration.
|
||||
|
||||
#### MISSING_NEXTAUTH_API_ROUTE_ERROR
|
||||
|
||||
This error happens when `[...nextauth].js` file is not found inside `pages/api/auth`.
|
||||
|
||||
Make sure the file is there and the filename is written correctly.
|
||||
|
||||
@@ -23,7 +23,7 @@ You can use also NextAuth.js with any database using a custom database adapter,
|
||||
|
||||
### What authentication services does NextAuth.js support?
|
||||
|
||||
NextAuth.js includes built-in support for signing in with Apple, Atlassian, Auth0, Google, Battle.net, Box, AWS Cognito, Discord, Facebook, FusionAuth, GitHub, GitLab, Google, Open ID Identity Server, Mixer, Okta, Slack, Spotify, Twitch, Twitter and Yandex.
|
||||
NextAuth.js includes built-in support for signing in with Apple, Atlassian, Auth0, Azure Active Directory B2C, Google, Battle.net, Box, AWS Cognito, Discord, Facebook, Foursquare, FusionAuth, GitHub, GitLab, Google, Open ID Identity Server, Mixer, Netlify, Okta, Slack, Spotify, Strava, Twitch, Twitter and Yandex.
|
||||
|
||||
NextAuth.js also supports email for passwordless sign in, which is useful for account recovery or for people who are not able to use an account with the configured OAuth services (e.g. due to service outage, account suspension or otherwise becoming locked out of an account).
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ To add NextAuth.js to a project create a file called `[...nextauth].js` in `page
|
||||
import NextAuth from 'next-auth'
|
||||
import Providers from 'next-auth/providers'
|
||||
|
||||
const options = {
|
||||
export default NextAuth({
|
||||
// Configure one or more authentication providers
|
||||
providers: [
|
||||
Providers.GitHub({
|
||||
@@ -33,9 +33,7 @@ const options = {
|
||||
|
||||
// A database is optional, but required to persist accounts in a database
|
||||
database: process.env.DATABASE_URL,
|
||||
}
|
||||
|
||||
export default (req, res) => NextAuth(req, res, options)
|
||||
})
|
||||
```
|
||||
|
||||
All requests to `/api/auth/*` (signin, callback, signout, etc) will automatically be handed by NextAuth.js.
|
||||
@@ -103,5 +101,5 @@ NEXTAUTH_URL=https://example.com
|
||||
:::tip
|
||||
In production, this needs to be set as an environment variable on the service you use to deploy your app.
|
||||
|
||||
To set environment variables on Vercel, you can use the [dashboard](https://vercel.com/dashboard) or the `now env` command.
|
||||
To set environment variables on Vercel, you can use the [dashboard](https://vercel.com/dashboard) or the `vercel env pull` [command](https://vercel.com/docs/build-step#development-environment-variables).
|
||||
:::
|
||||
|
||||
28
www/docs/providers/azure-ad-b2c.md
Normal file
28
www/docs/providers/azure-ad-b2c.md
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
id: azure-ad-b2c
|
||||
title: Azure Active Directory B2C
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
|
||||
|
||||
## Configuration
|
||||
|
||||
https://docs.microsoft.com/en-us/azure/active-directory-b2c/tutorial-create-tenant
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from 'next-auth/providers';
|
||||
...
|
||||
providers: [
|
||||
Providers.AzureADB2C({
|
||||
clientId: process.env.AZURE_CLIENT_ID,
|
||||
clientSecret: process.env.AZURE_CLIENT_SECRET,
|
||||
scope: 'offline_access User.Read',
|
||||
tenantId: process.env.AZURE_TENANT_ID,
|
||||
}),
|
||||
]
|
||||
...
|
||||
```
|
||||
132
www/docs/providers/bungie.md
Normal file
132
www/docs/providers/bungie.md
Normal file
@@ -0,0 +1,132 @@
|
||||
---
|
||||
id: bungie
|
||||
title: Bungie
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
https://github.com/Bungie-net/api/wiki/OAuth-Documentation
|
||||
|
||||
## Configuration
|
||||
|
||||
https://www.bungie.net/en/Application
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
...
|
||||
providers: [
|
||||
Providers.Bungie({
|
||||
clientId: process.env.BUNGIE_CLIENT_ID,
|
||||
clientSecret: process.env.BUNGIE_SECRET,
|
||||
apiKey: process.env.BUNGIE_API_KEY
|
||||
}),
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
## Instructions
|
||||
|
||||
### Configuration
|
||||
|
||||
:::tip
|
||||
Bungie require all sites to run HTTPS (including local development instances).
|
||||
:::
|
||||
|
||||
:::tip
|
||||
Bungie doesn't allow you to use localhost as the website URL, instead you need to use https://127.0.0.1:3000
|
||||
:::
|
||||
|
||||
Navigate to https://www.bungie.net/en/Application and fill in the required details:
|
||||
|
||||
* Application name
|
||||
* Application Status
|
||||
* Website
|
||||
* OAuth Client Type
|
||||
- Confidential
|
||||
* Redirect URL
|
||||
- https://localhost:3000/api/auth/callback/bungie
|
||||
* Scope
|
||||
- `Access items like your Bungie.net notifications, memberships, and recent Bungie.Net forum activity.`
|
||||
* Origin Header
|
||||
|
||||
The following guide may be helpful:
|
||||
|
||||
* [How to setup localhost with HTTPS with a Next.js app](https://medium.com/@anMagpie/secure-your-local-development-server-with-https-next-js-81ac6b8b3d68)
|
||||
|
||||
### Example server
|
||||
|
||||
You will need to edit your host file and point your site at `127.0.0.1`
|
||||
|
||||
[How to edit my host file?](https://phoenixnap.com/kb/how-to-edit-hosts-file-in-windows-mac-or-linux)
|
||||
|
||||
On Windows (Run Powershell as administrator)
|
||||
|
||||
```ps
|
||||
Add-Content -Path C:\Windows\System32\drivers\etc\hosts -Value "127.0.0.1`tdev.example.com" -Force
|
||||
```
|
||||
|
||||
```
|
||||
127.0.0.1 dev.example.com
|
||||
```
|
||||
|
||||
#### Create certificate
|
||||
|
||||
|
||||
Creating a certificate for localhost is easy with openssl . Just put the following command in the terminal. The output will be two files: localhost.key and localhost.crt.
|
||||
|
||||
```bash
|
||||
openssl req -x509 -out localhost.crt -keyout localhost.key \
|
||||
-newkey rsa:2048 -nodes -sha256 \
|
||||
-subj '/CN=localhost' -extensions EXT -config <( \
|
||||
printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth")
|
||||
```
|
||||
|
||||
:::tip
|
||||
**Windows**
|
||||
|
||||
The OpenSSL executable is distributed with [Git](https://git-scm.com/download/win]9) for Windows.
|
||||
Once installed you will find the openssl.exe file in `C:/Program Files/Git/mingw64/bin` which you can add to the system PATH environment variable if it’s not already done.
|
||||
|
||||
Add environment variable `OPENSSL_CONF=C:/Program Files/Git/mingw64/ssl/openssl.cnf`
|
||||
|
||||
```bash
|
||||
req -x509 -out localhost.crt -keyout localhost.key \
|
||||
-newkey rsa:2048 -nodes -sha256 \
|
||||
-subj '/CN=localhost'
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Create directory `certificates` and place `localhost.key` and `localhost.crt`
|
||||
|
||||
|
||||
You can create a `server.js` in the root of your project and run it with `node server.js` to test Sign in with Bungie integration locally:
|
||||
|
||||
|
||||
```js
|
||||
const { createServer } = require('https')
|
||||
const { parse } = require('url')
|
||||
const next = require('next')
|
||||
const fs = require('fs')
|
||||
|
||||
const dev = process.env.NODE_ENV !== 'production'
|
||||
const app = next({ dev })
|
||||
const handle = app.getRequestHandler()
|
||||
|
||||
const httpsOptions = {
|
||||
key: fs.readFileSync('./certificates/localhost.key'),
|
||||
cert: fs.readFileSync('./certificates/localhost.crt')
|
||||
}
|
||||
|
||||
app.prepare().then(() => {
|
||||
createServer(httpsOptions, (req, res) => {
|
||||
const parsedUrl = parse(req.url, true)
|
||||
handle(req, res, parsedUrl)
|
||||
}).listen(3000, err => {
|
||||
if (err) throw err
|
||||
console.log('> Ready on https://localhost:3000')
|
||||
})
|
||||
})
|
||||
```
|
||||
@@ -27,9 +27,9 @@ The Credentials provider is specified like other providers, except that you need
|
||||
|
||||
If you return `false` or `null` then an error will be displayed advising the user to check their details.
|
||||
|
||||
3. `Promise.Rejected()` with an Error or a URL.
|
||||
3. You can throw an Error or a URL (a string).
|
||||
|
||||
If you reject the promise with an Error, the user will be sent to the error page with the error message as a query parameter. If you reject the promise with a URL (a string), the user will be redirected to the URL.
|
||||
If you throw an Error, the user will be sent to the error page with the error message as a query parameter. If throw a URL (a string), the user will be redirected to the URL.
|
||||
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
import Providers from `next-auth/providers`
|
||||
@@ -51,13 +51,13 @@ providers: [
|
||||
|
||||
if (user) {
|
||||
// Any object returned will be saved in `user` property of the JWT
|
||||
return Promise.resolve(user)
|
||||
return user
|
||||
} else {
|
||||
// If you return null or false then the credentials will be rejected
|
||||
return Promise.resolve(null)
|
||||
return null
|
||||
// You can also Reject this callback with an Error or with a URL:
|
||||
// return Promise.reject(new Error('error message')) // Redirect to error page
|
||||
// return Promise.reject('/path/to/redirect') // Redirect to a URL
|
||||
// throw new Error('error message') // Redirect to error page
|
||||
// throw '/path/to/redirect' // Redirect to a URL
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -84,7 +84,7 @@ As with all providers, the order you specify them is the order they are displaye
|
||||
name: "Domain Account",
|
||||
authorize: async (credentials) => {
|
||||
const user = { /* add function to get user */ }
|
||||
return Promise.resolve(user)
|
||||
return user
|
||||
},
|
||||
credentials: {
|
||||
domain: { label: "Domain", type: "text ", placeholder: "CORPNET", value: "CORPNET" },
|
||||
@@ -97,7 +97,7 @@ As with all providers, the order you specify them is the order they are displaye
|
||||
name: "Two Factor Auth",
|
||||
authorize: async (credentials) => {
|
||||
const user = { /* add function to get user */ }
|
||||
return Promise.resolve(user)
|
||||
return user
|
||||
},
|
||||
credentials: {
|
||||
email: { label: "Username", type: "text ", placeholder: "jsmith" },
|
||||
|
||||
@@ -21,6 +21,6 @@ providers: [
|
||||
clientId: process.env.DISCORD_CLIENT_ID,
|
||||
clientSecret: process.env.DISCORD_CLIENT_SECRET
|
||||
})
|
||||
}
|
||||
]
|
||||
...
|
||||
```
|
||||
|
||||
30
www/docs/providers/foursquare.md
Normal file
30
www/docs/providers/foursquare.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
id: foursquare
|
||||
title: Foursquare
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
https://developer.foursquare.com/docs/places-api/authentication/#web-applications
|
||||
|
||||
## Configuration
|
||||
|
||||
https://developer.foursquare.com/
|
||||
|
||||
:::warning
|
||||
Foursquare requires an additional `apiVersion` parameter in [`YYYYMMDD` format](https://developer.foursquare.com/docs/places-api/versioning/), which essentially states "I'm prepared for API changes up to this date".
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
...
|
||||
providers: [
|
||||
Providers.Foursquare({
|
||||
clientId: process.env.FOURSQUARE_CLIENT_ID,
|
||||
clientSecret: process.env.FOURSQUARE_CLIENT_SECRET,
|
||||
apiVersion: 'YYYYMMDD'
|
||||
})
|
||||
}
|
||||
...
|
||||
```
|
||||
@@ -63,9 +63,9 @@ const options = {
|
||||
if (account.provider === 'google' &&
|
||||
profile.verified_email === true &&
|
||||
profile.email.endsWith('@example.com')) {
|
||||
return Promise.resolve(true)
|
||||
return true
|
||||
} else {
|
||||
return Promise.resolve(false)
|
||||
return false
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
26
www/docs/providers/netlify.md
Normal file
26
www/docs/providers/netlify.md
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
id: netlify
|
||||
title: Netlify
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
https://www.netlify.com/blog/2016/10/10/integrating-with-netlify-oauth2/
|
||||
|
||||
## Configuration
|
||||
|
||||
https://github.com/netlify/netlify-oauth-example
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
...
|
||||
providers: [
|
||||
Providers.Netlify({
|
||||
clientId: process.env.NETLIFY_CLIENT_ID,
|
||||
clientSecret: process.env.NETLIFY_CLIENT_SECRET
|
||||
})
|
||||
}
|
||||
...
|
||||
```
|
||||
22
www/docs/providers/strava.md
Normal file
22
www/docs/providers/strava.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
id: strava
|
||||
title: Strava
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
http://developers.strava.com/docs/reference/
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from 'next-auth/providers'
|
||||
...
|
||||
providers: [
|
||||
Providers.Strava({
|
||||
clientId: process.env.STRAVA_CLIENT_ID,
|
||||
clientSecret: process.env.STRAVA_CLIENT_SECRET,
|
||||
})
|
||||
}
|
||||
...
|
||||
```
|
||||
@@ -74,7 +74,7 @@ import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
const options = {
|
||||
export default NextAuth({
|
||||
providers: [
|
||||
Providers.Google({
|
||||
clientId: process.env.GOOGLE_CLIENT_ID,
|
||||
@@ -82,9 +82,7 @@ const options = {
|
||||
})
|
||||
],
|
||||
adapter: Adapters.Prisma.Adapter({ prisma }),
|
||||
}
|
||||
|
||||
export default (req, res) => NextAuth(req, res, options)
|
||||
})
|
||||
```
|
||||
|
||||
:::tip
|
||||
|
||||
@@ -58,10 +58,31 @@ CREATE TABLE verification_requests
|
||||
created_at datetime NOT NULL DEFAULT getdate(),
|
||||
updated_at datetime NOT NULL DEFAULT getdate()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX compound_id
|
||||
ON accounts(compound_id);
|
||||
|
||||
CREATE INDEX provider_account_id
|
||||
ON accounts(provider_account_id);
|
||||
|
||||
CREATE INDEX provider_id
|
||||
ON accounts(provider_id);
|
||||
|
||||
CREATE INDEX user_id
|
||||
ON accounts(user_id);
|
||||
|
||||
CREATE UNIQUE INDEX session_token
|
||||
ON sessions(session_token);
|
||||
|
||||
CREATE UNIQUE INDEX access_token
|
||||
ON sessions(access_token);
|
||||
|
||||
CREATE UNIQUE INDEX email
|
||||
ON users(email);
|
||||
|
||||
CREATE UNIQUE INDEX token
|
||||
ON verification_requests(token);
|
||||
```
|
||||
|
||||
:::warning
|
||||
The above schema is incomplete, it does not include indexes.
|
||||
|
||||
When using NextAuth.js with SQL Server fir the first time, run NextAuth.js once against your database with `?syncronize=true` on the connection string and export the schema that is created.
|
||||
:::
|
||||
When using NextAuth.js with SQL Server for the first time, run NextAuth.js once against your database with `?synchronize=true` on the connection string and export the schema that is created.
|
||||
:::
|
||||
|
||||
@@ -46,7 +46,7 @@ CREATE TABLE users
|
||||
name VARCHAR(255),
|
||||
email VARCHAR(255),
|
||||
email_verified TIMESTAMPTZ,
|
||||
image VARCHAR(255),
|
||||
image TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
@@ -87,4 +87,4 @@ CREATE UNIQUE INDEX email
|
||||
CREATE UNIQUE INDEX token
|
||||
ON verification_requests(token);
|
||||
|
||||
```
|
||||
```
|
||||
|
||||
@@ -149,7 +149,7 @@ const Adapter = (config, options = {}) => {
|
||||
return null
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
return {
|
||||
createUser,
|
||||
getUser,
|
||||
getUserByEmail,
|
||||
@@ -166,7 +166,7 @@ const Adapter = (config, options = {}) => {
|
||||
createVerificationRequest,
|
||||
getVerificationRequest,
|
||||
deleteVerificationRequest
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -14,7 +14,7 @@ const ldap = require("ldapjs");
|
||||
import NextAuth from "next-auth";
|
||||
import Providers from "next-auth/providers";
|
||||
|
||||
const options = {
|
||||
export default NextAuth({
|
||||
providers: [
|
||||
Providers.Credentials({
|
||||
name: "LDAP",
|
||||
@@ -64,9 +64,7 @@ const options = {
|
||||
secret: process.env.NEXTAUTH_SECRET,
|
||||
encryption: true, // Very important to encrypt the JWT, otherwise you're leaking username+password into the browser
|
||||
},
|
||||
};
|
||||
|
||||
export default (req, res) => NextAuth(req, res, options);
|
||||
});
|
||||
```
|
||||
|
||||
The idea is that once one is authenticated with the LDAP server, one can pass through both the username/DN and password to the JWT stored in the browser.
|
||||
|
||||
@@ -3,7 +3,7 @@ id: securing-pages-and-api-routes
|
||||
title: Securing pages and API routes
|
||||
---
|
||||
|
||||
You can easily protect client and server side side rendered pages and API routes with NextAuth.js.
|
||||
You can easily protect client and server side rendered pages and API routes with NextAuth.js.
|
||||
|
||||
_You can find working examples of the approaches shown below in the [example project](https://github.com/iaincollins/next-auth-example/)._
|
||||
|
||||
@@ -67,7 +67,7 @@ export async function getServerSideProps(context) {
|
||||
```
|
||||
|
||||
:::tip
|
||||
This example assumes you have configured `_app.js` to pass the `session` prop through so that it's immediately avalible on page load to `useSession`.
|
||||
This example assumes you have configured `_app.js` to pass the `session` prop through so that it's immediately available on page load to `useSession`.
|
||||
|
||||
```js title="pages/_app.js"
|
||||
import { Provider } from 'next-auth/client'
|
||||
|
||||
@@ -36,7 +36,7 @@ Second, a cypress file for environment variables. These can be defined in `cypre
|
||||
{
|
||||
"GOOGLE_USER": "username@company.com",
|
||||
"GOOGLE_PW": "password",
|
||||
"COOKIE_NAME": "__Secure-next-auth.session-token",
|
||||
"COOKIE_NAME": "next-auth.session-token",
|
||||
"SITE_NAME": "http://localhost:3000"
|
||||
}
|
||||
```
|
||||
@@ -111,8 +111,11 @@ describe('Login page', () => {
|
||||
})
|
||||
|
||||
Cypress.Cookies.defaults({
|
||||
whitelist: cookieName,
|
||||
preserve: cookieName,
|
||||
})
|
||||
|
||||
// remove the two lines below if you need to stay logged in
|
||||
// for your remaining tests
|
||||
cy.visit('/api/auth/signout')
|
||||
cy.get('form').submit()
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ import Adapters from "next-auth/adapters"
|
||||
|
||||
import Models from "../../../models"
|
||||
|
||||
const options = {
|
||||
export default NextAuth({
|
||||
providers: [
|
||||
// Your providers
|
||||
],
|
||||
@@ -77,9 +77,7 @@ const options = {
|
||||
},
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
export default (req, res) => NextAuth(req, res, options)
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ module.exports = {
|
||||
alt: 'NextAuth Logo',
|
||||
src: 'img/logo/logo-xs.png'
|
||||
},
|
||||
links: [
|
||||
items: [
|
||||
{
|
||||
to: '/getting-started/introduction',
|
||||
activeBasePath: 'docs',
|
||||
|
||||
7060
www/package-lock.json
generated
7060
www/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,16 +10,16 @@
|
||||
"lint:fix": "standard --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "^2.0.0-alpha.54",
|
||||
"@docusaurus/preset-classic": "^2.0.0-alpha.54",
|
||||
"@docusaurus/core": "^2.0.0-alpha.66",
|
||||
"@docusaurus/preset-classic": "^2.0.0-alpha.66",
|
||||
"classnames": "^2.2.6",
|
||||
"docusaurus-lunr-search": "^1.0.3",
|
||||
"jose": "^1.27.2",
|
||||
"docusaurus-lunr-search": "^2.1.7",
|
||||
"jose": "^2.0.2",
|
||||
"lodash.times": "^4.3.2",
|
||||
"react": "^16.8.4",
|
||||
"react-dom": "^16.8.4",
|
||||
"react-marquee-slider": "^1.0.16",
|
||||
"styled-components": "^5.1.1"
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-marquee-slider": "^1.1.2",
|
||||
"styled-components": "^5.2.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@@ -34,6 +34,6 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"standard": "^14.3.4"
|
||||
"standard": "^15.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,9 +26,11 @@ module.exports = {
|
||||
'providers/apple',
|
||||
'providers/atlassian',
|
||||
'providers/auth0',
|
||||
'providers/azure-ad-b2c',
|
||||
'providers/basecamp',
|
||||
'providers/battle.net',
|
||||
'providers/box',
|
||||
'providers/bungie',
|
||||
'providers/cognito',
|
||||
'providers/discord',
|
||||
'providers/email',
|
||||
@@ -44,6 +46,7 @@ module.exports = {
|
||||
'providers/okta',
|
||||
'providers/slack',
|
||||
'providers/spotify',
|
||||
'providers/strava',
|
||||
'providers/twitch',
|
||||
'providers/twitter',
|
||||
'providers/yandex'
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
/* You can override the default Infima variables here. */
|
||||
:root {
|
||||
--ifm-color-link: #289EF9;
|
||||
--ifm-color-link: #289ef9;
|
||||
--ifm-color-primary: #1eb1fc;
|
||||
--ifm-color-primary-dark: #03a7fa;
|
||||
--ifm-color-primary-darker: #039eed;
|
||||
@@ -21,22 +21,24 @@
|
||||
--ifm-color-info: #1eb1fc;
|
||||
--ifm-color-success: #1eb1fc;
|
||||
--ifm-color-warning: #c94b4b;
|
||||
--ifm-font-family-base: -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';
|
||||
--ifm-font-family-base: -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";
|
||||
--ifm-background-color: #fff;
|
||||
--ifm-footer-background-color: #f9f9f9;
|
||||
--ifm-hero-background-color: #f5f5f5;
|
||||
--ifm-navbar-background-color: rgba(255,255,255,0.95);
|
||||
--ifm-navbar-background-color: rgba(255, 255, 255, 0.95);
|
||||
--ifm-h1-font-size: 3rem;
|
||||
--ifm-h1-font-size: 2rem;
|
||||
}
|
||||
|
||||
html[data-theme='dark']:root {
|
||||
--ifm-color-link: #289EF9;
|
||||
html[data-theme="dark"]:root {
|
||||
--ifm-color-link: #289ef9;
|
||||
--ifm-footer-background-color: #111;
|
||||
--ifm-html-background-color: #242526;
|
||||
--ifm-background-color: #000000;
|
||||
--ifm-hero-background-color: #111111;
|
||||
--ifm-navbar-background-color: rgba(0,0,0,0.95);
|
||||
--ifm-navbar-background-color: rgba(0, 0, 0, 0.95);
|
||||
}
|
||||
|
||||
@import "hero.css";
|
||||
@@ -63,7 +65,6 @@ a {
|
||||
a,
|
||||
a:hover,
|
||||
.navbar .navbar__link:hover {
|
||||
text-decoration: underline;
|
||||
color: var(--ifm-color-link);
|
||||
}
|
||||
|
||||
@@ -109,12 +110,12 @@ a:hover,
|
||||
|
||||
.navbar__title {
|
||||
font-size: 1.2rem;
|
||||
margin-left: .2rem;
|
||||
margin-left: 0.2rem;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navbar-sidebar__brand .navbar__title {
|
||||
display: inline;
|
||||
.navbar-sidebar__brand .navbar__title {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 420px) {
|
||||
@@ -137,14 +138,14 @@ a:hover,
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
font-size: 0.9rem;
|
||||
background: #1E1E1E;
|
||||
background: #1e1e1e;
|
||||
overflow: hidden;
|
||||
border-radius: .5rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.home-main .code .code-heading {
|
||||
color: rgba(255,255,255,1);
|
||||
background: #191A1B;
|
||||
color: rgba(255, 255, 255, 1);
|
||||
background: #191a1b;
|
||||
margin: 0;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 1rem;
|
||||
@@ -171,7 +172,7 @@ hr {
|
||||
border-color: #ddd;
|
||||
}
|
||||
|
||||
html[data-theme='dark'] hr {
|
||||
html[data-theme="dark"] hr {
|
||||
border-color: #242526;
|
||||
}
|
||||
|
||||
@@ -190,7 +191,7 @@ html[data-theme='dark'] hr {
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
html[data-theme='dark'] .navbar__item.navbar__link[href*="github"]:before {
|
||||
html[data-theme="dark"] .navbar__item.navbar__link[href*="github"]:before {
|
||||
background-image: url("/img/brand-github-inverted.svg");
|
||||
}
|
||||
|
||||
@@ -198,12 +199,12 @@ html[data-theme='dark'] .navbar__item.navbar__link[href*="github"]:before {
|
||||
content: "";
|
||||
width: 3rem;
|
||||
height: 1.2rem;
|
||||
margin-top: .2rem;
|
||||
margin-top: 0.2rem;
|
||||
background-image: url("/img/brand-npm.svg");
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
html[data-theme='dark'] .navbar__item.navbar__link[href*="npm"]:before {
|
||||
html[data-theme="dark"] .navbar__item.navbar__link[href*="npm"]:before {
|
||||
background-image: url("/img/brand-npm-inverted.svg");
|
||||
}
|
||||
|
||||
@@ -214,8 +215,8 @@ html[data-theme='dark'] .navbar__item.navbar__link[href*="npm"]:before {
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme='dark'] {
|
||||
--ifm-background-color: #1F201C;
|
||||
html[data-theme="dark"] {
|
||||
--ifm-background-color: #1f201c;
|
||||
}
|
||||
|
||||
.home-main .feature-image-wrapper {
|
||||
@@ -225,22 +226,21 @@ html[data-theme='dark'] {
|
||||
border-radius: 10rem;
|
||||
overflow: visible;
|
||||
background-color: var(--ifm-color-primary);
|
||||
box-shadow: 0 0 2rem rgba(0,0,0,.1);
|
||||
background-image: linear-gradient(0deg, #1786FB 0%, #45EABA 100%);
|
||||
box-shadow: 0 0 2rem rgba(0, 0, 0, 0.1);
|
||||
background-image: linear-gradient(0deg, #1786fb 0%, #45eaba 100%);
|
||||
}
|
||||
|
||||
.home-main .section-features .row .col:nth-child(2) .feature-image-wrapper {
|
||||
background-image: linear-gradient(0deg, #605BD4 0%, #E95595 100%);
|
||||
background-image: linear-gradient(0deg, #605bd4 0%, #e95595 100%);
|
||||
}
|
||||
|
||||
|
||||
.home-main .section-features .row .col:nth-child(3) .feature-image-wrapper {
|
||||
background-image: linear-gradient(0deg, #FD5D21 0%, #FBB12E 100%);
|
||||
background-image: linear-gradient(0deg, #fd5d21 0%, #fbb12e 100%);
|
||||
}
|
||||
|
||||
.home-main .feature-image-wrapper img {
|
||||
filter: saturate(1.2) contrast(2);
|
||||
zoom: .80;
|
||||
zoom: 0.8;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 996px) {
|
||||
@@ -255,14 +255,14 @@ html[data-theme='dark'] {
|
||||
font-size: 1rem !important;
|
||||
color: #000;
|
||||
text-align: left;
|
||||
padding: .1rem 2rem;
|
||||
padding: 0.1rem 2rem;
|
||||
text-decoration: underline;
|
||||
font-weight: bold;
|
||||
display: inline;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
html[data-theme='dark'] .button.button--secondary.button--sm.menu__button {
|
||||
html[data-theme="dark"] .button.button--secondary.button--sm.menu__button {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@@ -271,4 +271,4 @@ html[data-theme='dark'] {
|
||||
opacity: 0.8;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
.menu__list .menu__link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.menu__list .menu__list-item a[href="#!"],
|
||||
.menu__list .menu__list-item a:not(.menu__link--sublist) {
|
||||
background: transparent;
|
||||
@@ -22,13 +26,18 @@
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.menu__list .menu__list-item a.menu__link.menu__link--active:not(.menu__link--sublist) {
|
||||
background: transparent;
|
||||
.menu__list
|
||||
.menu__list-item
|
||||
a.menu__link.menu__link--active:not(.menu__link--sublist) {
|
||||
background: transparent;
|
||||
color: var(--ifm-color-link);
|
||||
}
|
||||
|
||||
html[data-theme='dark'] .menu__list .menu__list-item a:not(.menu__link--sublist),
|
||||
html[data-theme='dark'] .menu__list .menu__list-item a[href="#!"] {
|
||||
html[data-theme="dark"]
|
||||
.menu__list
|
||||
.menu__list-item
|
||||
a:not(.menu__link--sublist),
|
||||
html[data-theme="dark"] .menu__list .menu__list-item a[href="#!"] {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.table-of-contents__link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.contents a {
|
||||
text-decoration: none;
|
||||
}
|
||||
@@ -36,10 +40,10 @@
|
||||
}
|
||||
|
||||
.contents li > ul {
|
||||
margin-left: .25rem;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.contents li > ul li > a {
|
||||
font-weight: 400;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ const serverlessFunctionCode = `
|
||||
import NextAuth from 'next-auth'
|
||||
import Providers from 'next-auth/providers'
|
||||
|
||||
const options = {
|
||||
export default NextAuth({
|
||||
providers: [
|
||||
// OAuth authentication providers...
|
||||
Providers.Apple({
|
||||
@@ -242,9 +242,7 @@ const options = {
|
||||
],
|
||||
// Optional SQL or MongoDB database to persist users
|
||||
database: process.env.DATABASE_URL
|
||||
}
|
||||
|
||||
export default (req, res) => NextAuth(req, res, options)
|
||||
})
|
||||
`.trim()
|
||||
|
||||
export default Home
|
||||
|
||||
@@ -5,95 +5,95 @@
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, { useRef, useCallback } from 'react'
|
||||
import classnames from 'classnames'
|
||||
import { useHistory } from '@docusaurus/router'
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext'
|
||||
let loaded = false
|
||||
const Search = props => {
|
||||
const initialized = useRef(false)
|
||||
const searchBarRef = useRef(null)
|
||||
const history = useHistory()
|
||||
const { siteConfig = {} } = useDocusaurusContext()
|
||||
const { baseUrl } = siteConfig
|
||||
|
||||
import React, { useRef, useCallback } from "react";
|
||||
import classnames from "classnames";
|
||||
import { useHistory } from "@docusaurus/router";
|
||||
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
|
||||
let loaded = false;
|
||||
const Search = (props) => {
|
||||
const initialized = useRef(false);
|
||||
const searchBarRef = useRef(null);
|
||||
const history = useHistory();
|
||||
const { siteConfig = {} } = useDocusaurusContext();
|
||||
const { baseUrl } = siteConfig;
|
||||
const initAlgolia = () => {
|
||||
if (!initialized.current) {
|
||||
new window.DocSearch({
|
||||
searchData: window.searchData,
|
||||
inputSelector: '#search_input_react',
|
||||
inputSelector: "#search_input_react",
|
||||
// Override algolia's default selection event, allowing us to do client-side
|
||||
// navigation and avoiding a full page refresh.
|
||||
handleSelected: (_input, _event, suggestion) => {
|
||||
const url = baseUrl + suggestion.url
|
||||
const url = baseUrl + suggestion.url;
|
||||
// Use an anchor tag to parse the absolute url into a relative url
|
||||
// Alternatively, we can use new URL(suggestion.url) but its not supported in IE
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
// Algolia use closest parent element id #__docusaurus when a h1 page title does not have an id
|
||||
// So, we can safely remove it. See https://github.com/facebook/docusaurus/issues/1828 for more details.
|
||||
|
||||
history.push(url)
|
||||
}
|
||||
})
|
||||
initialized.current = true
|
||||
history.push(url);
|
||||
},
|
||||
});
|
||||
initialized.current = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getSearchData = () =>
|
||||
process.env.NODE_ENV === 'production'
|
||||
? fetch(`${baseUrl}search-data.json`).then((content) => content.json())
|
||||
: Promise.resolve([])
|
||||
process.env.NODE_ENV === "production"
|
||||
? fetch(`${baseUrl}search-doc.json`).then((content) => content.json())
|
||||
: Promise.resolve([]);
|
||||
|
||||
const loadAlgolia = () => {
|
||||
if (!loaded) {
|
||||
Promise.all([
|
||||
getSearchData(),
|
||||
import('./lib/DocSearch'),
|
||||
import('./algolia.css')
|
||||
import("./lib/DocSearch"),
|
||||
import("./algolia.css"),
|
||||
]).then(([searchData, { default: DocSearch }]) => {
|
||||
loaded = true
|
||||
window.searchData = searchData
|
||||
window.DocSearch = DocSearch
|
||||
initAlgolia()
|
||||
})
|
||||
loaded = true;
|
||||
window.searchData = searchData;
|
||||
window.DocSearch = DocSearch;
|
||||
initAlgolia();
|
||||
});
|
||||
} else {
|
||||
initAlgolia()
|
||||
initAlgolia();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSearchIconClick = useCallback(
|
||||
e => {
|
||||
(e) => {
|
||||
if (!searchBarRef.current.contains(e.target)) {
|
||||
searchBarRef.current.focus()
|
||||
searchBarRef.current.focus();
|
||||
}
|
||||
|
||||
props.handleSearchBarToggle(!props.isSearchBarExpanded)
|
||||
props.handleSearchBarToggle(!props.isSearchBarExpanded);
|
||||
},
|
||||
[props.isSearchBarExpanded]
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='navbar__search' key='search-box'>
|
||||
<div className="navbar__search" key="search-box">
|
||||
<span
|
||||
aria-label='expand searchbar'
|
||||
role='button'
|
||||
className={classnames('search-icon', {
|
||||
'search-icon-hidden': props.isSearchBarExpanded
|
||||
aria-label="expand searchbar"
|
||||
role="button"
|
||||
className={classnames("search-icon", {
|
||||
"search-icon-hidden": props.isSearchBarExpanded,
|
||||
})}
|
||||
onClick={toggleSearchIconClick}
|
||||
onKeyDown={toggleSearchIconClick}
|
||||
tabIndex={0}
|
||||
/>
|
||||
<input
|
||||
id='search_input_react'
|
||||
type='search'
|
||||
placeholder='Search'
|
||||
aria-label='Search'
|
||||
id="search_input_react"
|
||||
type="search"
|
||||
placeholder="Search"
|
||||
aria-label="Search"
|
||||
className={classnames(
|
||||
'navbar__search-input',
|
||||
{ 'search-bar-expanded': props.isSearchBarExpanded },
|
||||
{ 'search-bar': !props.isSearchBarExpanded }
|
||||
"navbar__search-input",
|
||||
{ "search-bar-expanded": props.isSearchBarExpanded },
|
||||
{ "search-bar": !props.isSearchBarExpanded }
|
||||
)}
|
||||
onClick={loadAlgolia}
|
||||
onMouseOver={loadAlgolia}
|
||||
@@ -102,7 +102,7 @@ const Search = props => {
|
||||
ref={searchBarRef}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Search
|
||||
export default Search;
|
||||
|
||||
Reference in New Issue
Block a user