mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0eb4159737 | ||
|
|
9f0008375f | ||
|
|
0cf1823e70 | ||
|
|
7f39669053 | ||
|
|
7b82d6e985 | ||
|
|
53b0a7aa74 | ||
|
|
fbb09303af | ||
|
|
ff05ac1e41 | ||
|
|
a6f6c1590d | ||
|
|
52c2466b9e |
@@ -1,4 +0,0 @@
|
||||
# Exclude directories we don't need from Docker context to improve build time
|
||||
node_modules
|
||||
www
|
||||
src
|
||||
49
.github/ISSUE_TEMPLATE/bug_report.md
vendored
49
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -2,33 +2,42 @@
|
||||
name: Bug report
|
||||
about: Report a defect with NextAuth.js
|
||||
labels: bug
|
||||
assignees: ''
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of the bug in NextAuth.js.
|
||||
## Description 🐜
|
||||
|
||||
Do not report bugs with your own project here, ask from help by raising a question instead - this helps us a lot with administration overhead.
|
||||
Please provide a clear and concise description of the bug in NextAuth.js.
|
||||
|
||||
**Steps to reproduce**
|
||||
Steps to reproduce the behavior.
|
||||
🚧 – _Do not report bugs with your own project here; ask for help [by raising a question instead](https://github.com/nextauthjs/next-auth/issues/new?assignees=&labels=question&template=question.md) - this helps us a lot with administration overhead._
|
||||
|
||||
Include a link to public repository which can be used to reproduce the behaviour.
|
||||
## How to reproduce ☕️
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
We encourage you to use one of the templates set up on **CodeSandbox** to reproduce your issue:
|
||||
|
||||
**Screenshots or error logs**
|
||||
If applicable add screenshots or error logs to help explain the problem.
|
||||
- [`next-auth-example`](https://codesandbox.io/s/next-auth-example-1kktb)
|
||||
- [`next-auth-typescript-example`](https://codesandbox.io/s/next-auth-typescript-example-se32w)
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
🚧 – _If you don't provide any way to reproduce the bug, the issue is at risk of being closed._
|
||||
|
||||
**Feedback**
|
||||
*Documentation refers to searching through [online documentation](https://next-auth.js.org), code comments and issue history. The example project refers to [next-auth-example](https://github.com/iaincollins/next-auth-example).*
|
||||
## Screenshots / Logs 📽
|
||||
|
||||
* [ ] Found the documentation helpful
|
||||
* [ ] Found documentation but was incomplete
|
||||
* [ ] Could not find relevant documentation
|
||||
* [ ] Found the example project helpful
|
||||
* [ ] Did not find the example project helpful
|
||||
**Help us help you**. We can address the bug you found much faster if you provide contextual screenshots or screen recordings showcasing the issue.
|
||||
|
||||
See [Kap](https://getkap.co/) for a good, easy-to-use, cross-platform screen recording tool.
|
||||
|
||||
## Environment 🖥
|
||||
|
||||
Please run this command:
|
||||
|
||||
```
|
||||
$ npx envinfo --system --binaries --browsers --npmPackages "{next-auth}"
|
||||
```
|
||||
|
||||
and paste the output here.
|
||||
|
||||
## Contributing 🙌🏽
|
||||
|
||||
It takes a lot of work 🏋🏻♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
|
||||
|
||||
In case you're willing to help fix this bug, please let us know here, and we'll reach you 😊 . Otherwise, you can have a look at the issues labelled with [`"good first issue"`](https://github.com/nextauthjs/next-auth/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and pick any of them.
|
||||
|
||||
41
.github/ISSUE_TEMPLATE/feature_request.md
vendored
41
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -2,25 +2,38 @@
|
||||
name: Feature request
|
||||
about: Suggest an idea for NextAuth.js
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Summary of proposed feature**
|
||||
A clear and concise description of the feature being proposed.
|
||||
## Summary 💭
|
||||
|
||||
**Purpose of proposed feature**
|
||||
A clear and concise description of why this feature is necessary and what problems it solves.
|
||||
A clear and concise summary of the feature being proposed.
|
||||
|
||||
**Detail about proposed feature**
|
||||
A detailed description of how the proposal might work (if you have one).
|
||||
## Description 📓
|
||||
|
||||
**Potential problems**
|
||||
Describe any potential problems or potential limitations or caveats that might apply to the proposed solution.
|
||||
Please provide a more in-depth description of the feature proposed.
|
||||
|
||||
**Describe any alternatives you've considered**
|
||||
A clear and concise description of any alternative options you've considered.
|
||||
Make sure you provide plenty of [links]() to external documentation and inline code examples like so:
|
||||
|
||||
**Additional context**
|
||||
Any other context, screenshots, etc.
|
||||
```js
|
||||
function myAwesomeNextAuthFeature() {
|
||||
return 💚
|
||||
}
|
||||
```
|
||||
|
||||
*Please indicate if you are willing and able to help implement the proposed feature.*
|
||||
Take time thinking about what you want to say and help us understand your proposal making sure that this description contains:
|
||||
|
||||
- **purpose of the feature**
|
||||
- **potential problems**
|
||||
- **potential alternatives**
|
||||
|
||||
You can use one of the templates set up on **CodeSandbox** to better illustrate your idea:
|
||||
|
||||
- [`next-auth-example`](https://codesandbox.io/s/next-auth-example-1kktb)
|
||||
- [`next-auth-typescript-example`](https://codesandbox.io/s/next-auth-typescript-example-se32w)
|
||||
|
||||
## Contributing 🙌🏽
|
||||
|
||||
It takes a lot of work 🏋🏻♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
|
||||
|
||||
In case you're willing to help implement this feature, please let us know here, and we'll reach you 😊 . Otherwise, you can have a look at the issues labelled with [`"good first issue"`](https://github.com/nextauthjs/next-auth/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and pick any of them.
|
||||
|
||||
37
.github/ISSUE_TEMPLATE/question.md
vendored
37
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -2,24 +2,31 @@
|
||||
name: Question
|
||||
about: Ask a question about NextAuth.js or for help using it
|
||||
labels: question
|
||||
assignees: ''
|
||||
assignees: ""
|
||||
---
|
||||
<!-- NOTE: Questions will be converted to Discussions. You can find them at https://github.com/nextauthjs/next-auth/discussions! -->
|
||||
|
||||
**Your question**
|
||||
<!-- A clear and concise question. -->
|
||||
## Question 💬
|
||||
|
||||
**What are you trying to do**
|
||||
<!-- A description of what you are trying to do, for context. -->
|
||||
Please provide an in-depth description of the question you have.
|
||||
|
||||
**Reproduction**
|
||||
<!-- If your question is code related, adding a reproduction to your use case can greatly reduce the time it takes us to figure out how to better help you. -->
|
||||
Make sure you [link]() to external documentation if necessary and provide inline code examples like so:
|
||||
|
||||
**Feedback**
|
||||
*Documentation refers to searching through [online documentation](https://next-auth.js.org), code comments and issue history. The example project refers to [next-auth-example](https://github.com/iaincollins/next-auth-example).*
|
||||
```js
|
||||
function myAwesomeNextAuthFeature() {
|
||||
return 💚
|
||||
}
|
||||
```
|
||||
|
||||
* [ ] Found the documentation helpful
|
||||
* [ ] Found documentation but was incomplete
|
||||
* [ ] Could not find relevant documentation
|
||||
* [ ] Found the example project helpful
|
||||
* [ ] Did not find the example project helpful
|
||||
**NOTE:** Questions will be converted to Discussions. You can find them [here](https://github.com/nextauthjs/next-auth/discussions)!
|
||||
|
||||
## How to reproduce ☕️
|
||||
|
||||
We encourage you to use the template set-up on **CodeSandbox** as a playground to represent your question or doubt:
|
||||
|
||||
- [`next-auth-example`](https://codesandbox.io/s/next-auth-example-1kktb)
|
||||
|
||||
## Contributing 🙌🏽
|
||||
|
||||
It takes a lot of work 🏋🏻♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
|
||||
|
||||
In case you're willing to help answer this question, please let us know here, and we'll reach you 😊 . Otherwise, you can have a look at the issues labelled with [`"good first issue"`](https://github.com/nextauthjs/next-auth/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and pick any of them.
|
||||
|
||||
36
.github/ISSUE_TEMPLATE/typescript.md
vendored
Normal file
36
.github/ISSUE_TEMPLATE/typescript.md
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: TypeScript
|
||||
about: Ask a question about NextAuth.js TypeScript integration
|
||||
labels:
|
||||
- question
|
||||
- TypeScript
|
||||
assignees:
|
||||
- lluia
|
||||
- balazsorban44
|
||||
---
|
||||
|
||||
## Question 💬
|
||||
|
||||
Please provide an in-depth description of the question you have when using NextAuth.js on a Typescript project or when consuming the built-in types for `next-auth`.
|
||||
|
||||
Make sure you [link]() to external documentation if necessary and provide inline code examples like so:
|
||||
|
||||
```js
|
||||
function myAwesomeNextAuthFeature() {
|
||||
return 💚
|
||||
}
|
||||
```
|
||||
|
||||
**NOTE:** Questions will be converted to Discussions. You can find them [here](https://github.com/nextauthjs/next-auth/discussions)!
|
||||
|
||||
## How to reproduce ☕️
|
||||
|
||||
We encourage you to use the template set-up on **CodeSandbox** as a playground to represent your question or doubt:
|
||||
|
||||
- [`next-auth-typescript-example`](https://codesandbox.io/s/next-auth-typescript-example-se32w)
|
||||
|
||||
## Contributing 🙌🏽
|
||||
|
||||
It takes a lot of work 🏋🏻♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
|
||||
|
||||
In case you're willing to help answer this TypeScript question, please let us know here, and we'll reach you 😊 . Otherwise, you can have a look at the issues labelled with [`"good first issue"`](https://github.com/nextauthjs/next-auth/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and pick any of them.
|
||||
29
.github/PULL_REQUEST_TEMPLATE.md
vendored
29
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -16,26 +16,29 @@ merge of your pull request!
|
||||
|
||||
<!-- What changes are being made? (What feature/bug is being fixed here?) -->
|
||||
|
||||
**What**:
|
||||
## Reasoning 💡
|
||||
|
||||
<!-- Why are these changes necessary? -->
|
||||
What changes are being made? What feature/bug is being fixed here?
|
||||
|
||||
**Why**:
|
||||
## Checklist 🧢
|
||||
|
||||
<!-- How were these changes implemented? -->
|
||||
Feel free cross items ( like this `~[] item~` ) if they're irrelevant to your changes.
|
||||
|
||||
**How**:
|
||||
|
||||
<!-- Have you done all of these things? -->
|
||||
|
||||
**Checklist**:
|
||||
|
||||
<!-- add "N/A" to the end of each line that's irrelevant to your changes -->
|
||||
<!-- to check an item, place an "x" in the box like so: "- [x] Documentation" -->
|
||||
To check an item, place an `x` in the box like so: `- [x] Documentation`.
|
||||
|
||||
- [ ] Documentation
|
||||
- [ ] Tests
|
||||
- [ ] Ready to be merged
|
||||
<!-- In your opinion, is this ready to be merged as soon as it's reviewed? -->
|
||||
|
||||
<!-- feel free to add additional comments -->
|
||||
## Affected issues 🎟
|
||||
|
||||
Please [scout and link issues](https://github.com/nextauthjs/next-auth/issues) that might be solved by this PR.
|
||||
|
||||
If you write `"Fixes"` or `"Closes"` before the issue link like so:
|
||||
|
||||
```
|
||||
Fixes #359
|
||||
```
|
||||
|
||||
the connected issue will be automatically closed once the PR is merged and hence help with maintenance of the library 😊
|
||||
|
||||
15
package-lock.json
generated
15
package-lock.json
generated
@@ -2508,6 +2508,21 @@
|
||||
"integrity": "sha512-CAEbWH7OIur6jEOzaai83jq3FmKmv4PmX1JYfs9IrYcGEVI/lyL1EXJGCj7eFVJ0bg5QR8LMxBlEtA+xKiLpFw==",
|
||||
"dev": true
|
||||
},
|
||||
"@next-auth/prisma-legacy-adapter": {
|
||||
"version": "0.0.1-canary.115",
|
||||
"resolved": "https://registry.npmjs.org/@next-auth/prisma-legacy-adapter/-/prisma-legacy-adapter-0.0.1-canary.115.tgz",
|
||||
"integrity": "sha512-rIIisYBvVtxDbY9Lbm+HOLbZyOaaEmtGc9wDN3tJLDUu3sLJOXNN7Pz29ThS+gf2lpMxXnfvk587hxGrnhCghQ=="
|
||||
},
|
||||
"@next-auth/typeorm-legacy-adapter": {
|
||||
"version": "0.0.2-canary.116",
|
||||
"resolved": "https://registry.npmjs.org/@next-auth/typeorm-legacy-adapter/-/typeorm-legacy-adapter-0.0.2-canary.116.tgz",
|
||||
"integrity": "sha512-06knawdYdHkiMVw5GfR6Ku5ryITdaOLWIGCbRwAtgCZE5Pf6am+nhnqQVeGq18odu84SmzA+hTtkGSAYbR6MIA==",
|
||||
"requires": {
|
||||
"crypto-js": "^4.0.0",
|
||||
"require_optional": "^1.0.1",
|
||||
"typeorm": "^0.2.30"
|
||||
}
|
||||
},
|
||||
"@next/env": {
|
||||
"version": "10.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-10.0.5.tgz",
|
||||
|
||||
17
package.json
17
package.json
@@ -37,22 +37,9 @@
|
||||
"watch": "npm run watch:js | npm run watch:css",
|
||||
"watch:js": "babel --config-file ./config/babel.config.json --watch src --out-dir dist",
|
||||
"watch:css": "postcss --config config/postcss.config.js --watch src/**/*.css --base src --dir dist",
|
||||
"test:app:start": "docker-compose -f test/docker/app.yml up -d",
|
||||
"test:app:rebuild": "npm run build && docker-compose -f test/docker/app.yml up -d --build",
|
||||
"test:app:stop": "docker-compose -f test/docker/app.yml down",
|
||||
"test": "npm run test:app:rebuild && npm run test:integration && npm run test:app:stop && npm run test:types",
|
||||
"test:db": "npm run test:db:mysql && npm run test:db:postgres && npm run test:db:mongodb && npm run test:db:mssql",
|
||||
"test:db:mysql": "node test/mysql.js",
|
||||
"test:db:postgres": "node test/postgres.js",
|
||||
"test:db:mongodb": "node test/mongodb.js",
|
||||
"test:db:mssql": "node test/mssql.js",
|
||||
"test:integration": "mocha test/integration",
|
||||
"test": "echo \"Write some tests...\"; npm run test:types",
|
||||
"test:types": "dtslint types",
|
||||
"db:start": "docker-compose -f test/docker/databases.yml up -d",
|
||||
"db:stop": "docker-compose -f test/docker/databases.yml down",
|
||||
"prepublishOnly": "npm run build",
|
||||
"publish:beta": "npm publish --tag beta",
|
||||
"publish:canary": "npm publish --tag canary",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix"
|
||||
},
|
||||
@@ -74,6 +61,8 @@
|
||||
],
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@next-auth/prisma-legacy-adapter": "canary",
|
||||
"@next-auth/typeorm-legacy-adapter": "canary",
|
||||
"crypto-js": "^4.0.0",
|
||||
"futoin-hkdf": "^1.3.2",
|
||||
"jose": "^1.27.2",
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
const Adapter = (config, options = {}) => {
|
||||
async function getAdapter (appOptions) {
|
||||
const { logger } = appOptions
|
||||
// Display debug output if debug option enabled
|
||||
function debug (debugCode, ...args) {
|
||||
logger.debug(`ADAPTER_${debugCode}`, ...args)
|
||||
}
|
||||
|
||||
async function createUser (profile) {
|
||||
debug('createUser', profile)
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUser (id) {
|
||||
debug('getUser', id)
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUserByEmail (email) {
|
||||
debug('getUserByEmail', email)
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUserByProviderAccountId (providerId, providerAccountId) {
|
||||
debug('getUserByProviderAccountId', providerId, providerAccountId)
|
||||
return null
|
||||
}
|
||||
|
||||
async function updateUser (user) {
|
||||
debug('updateUser', user)
|
||||
return null
|
||||
}
|
||||
|
||||
async function deleteUser (userId) {
|
||||
debug('deleteUser', userId)
|
||||
return null
|
||||
}
|
||||
|
||||
async function linkAccount (userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires) {
|
||||
debug('linkAccount', userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires)
|
||||
return null
|
||||
}
|
||||
|
||||
async function unlinkAccount (userId, providerId, providerAccountId) {
|
||||
debug('unlinkAccount', userId, providerId, providerAccountId)
|
||||
return null
|
||||
}
|
||||
|
||||
async function createSession (user) {
|
||||
debug('createSession', user)
|
||||
return null
|
||||
}
|
||||
|
||||
async function getSession (sessionToken) {
|
||||
debug('getSession', sessionToken)
|
||||
return null
|
||||
}
|
||||
|
||||
async function updateSession (session, force) {
|
||||
debug('updateSession', session)
|
||||
return null
|
||||
}
|
||||
|
||||
async function deleteSession (sessionToken) {
|
||||
debug('deleteSession', sessionToken)
|
||||
return null
|
||||
}
|
||||
|
||||
async function createVerificationRequest (identifier, url, token, secret, provider) {
|
||||
debug('createVerificationRequest', identifier)
|
||||
return null
|
||||
}
|
||||
|
||||
async function getVerificationRequest (identifier, token, secret, provider) {
|
||||
debug('getVerificationRequest', identifier, token)
|
||||
return null
|
||||
}
|
||||
|
||||
async function deleteVerificationRequest (identifier, token, secret, provider) {
|
||||
debug('deleteVerification', identifier, token)
|
||||
return null
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
createUser,
|
||||
getUser,
|
||||
getUserByEmail,
|
||||
getUserByProviderAccountId,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
linkAccount,
|
||||
unlinkAccount,
|
||||
createSession,
|
||||
getSession,
|
||||
updateSession,
|
||||
deleteSession,
|
||||
createVerificationRequest,
|
||||
getVerificationRequest,
|
||||
deleteVerificationRequest
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
getAdapter
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
Adapter
|
||||
}
|
||||
8
src/adapters/prisma.js
Normal file
8
src/adapters/prisma.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
* Source code is now at:
|
||||
* https://github.com/nextauthjs/adapters/tree/canary/packages/prisma-legacy
|
||||
*/
|
||||
|
||||
import PrismaLegacyAdapter from "@next-auth/prisma-legacy-adapter"
|
||||
|
||||
export default PrismaLegacyAdapter
|
||||
@@ -1,340 +0,0 @@
|
||||
import { createHash, randomBytes } from 'crypto'
|
||||
|
||||
import { CreateUserError } from '../../lib/errors'
|
||||
|
||||
const Adapter = (config) => {
|
||||
const {
|
||||
prisma,
|
||||
modelMapping = {
|
||||
User: 'user',
|
||||
Account: 'account',
|
||||
Session: 'session',
|
||||
VerificationRequest: 'verificationRequest'
|
||||
}
|
||||
} = config
|
||||
|
||||
const { User, Account, Session, VerificationRequest } = modelMapping
|
||||
|
||||
function getCompoundId (providerId, providerAccountId) {
|
||||
return createHash('sha256').update(`${providerId}:${providerAccountId}`).digest('hex')
|
||||
}
|
||||
|
||||
async function getAdapter (appOptions) {
|
||||
const { logger } = appOptions
|
||||
function debug (debugCode, ...args) {
|
||||
logger.debug(`PRISMA_${debugCode}`, ...args)
|
||||
}
|
||||
|
||||
if (appOptions && (!appOptions.session || !appOptions.session.maxAge)) {
|
||||
debug('GET_ADAPTER', 'Session expiry not configured (defaulting to 30 days')
|
||||
}
|
||||
|
||||
const defaultSessionMaxAge = 30 * 24 * 60 * 60 * 1000
|
||||
const sessionMaxAge = (appOptions && appOptions.session && appOptions.session.maxAge)
|
||||
? appOptions.session.maxAge * 1000
|
||||
: defaultSessionMaxAge
|
||||
const sessionUpdateAge = (appOptions && appOptions.session && appOptions.session.updateAge)
|
||||
? appOptions.session.updateAge * 1000
|
||||
: 0
|
||||
|
||||
async function createUser (profile) {
|
||||
debug('CREATE_USER', profile)
|
||||
try {
|
||||
return prisma[User].create({
|
||||
data: {
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: profile.image,
|
||||
emailVerified: profile.emailVerified ? profile.emailVerified.toISOString() : null
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('CREATE_USER_ERROR', error)
|
||||
return Promise.reject(new CreateUserError(error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getUser (id) {
|
||||
debug('GET_USER', id)
|
||||
try {
|
||||
return prisma[User].findUnique({ where: { id } })
|
||||
} catch (error) {
|
||||
logger.error('GET_USER_BY_ID_ERROR', error)
|
||||
return Promise.reject(new Error('GET_USER_BY_ID_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getUserByEmail (email) {
|
||||
debug('GET_USER_BY_EMAIL', email)
|
||||
try {
|
||||
if (!email) { return Promise.resolve(null) }
|
||||
return prisma[User].findUnique({ where: { email } })
|
||||
} catch (error) {
|
||||
logger.error('GET_USER_BY_EMAIL_ERROR', error)
|
||||
return Promise.reject(new Error('GET_USER_BY_EMAIL_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getUserByProviderAccountId (providerId, providerAccountId) {
|
||||
debug('GET_USER_BY_PROVIDER_ACCOUNT_ID', providerId, providerAccountId)
|
||||
try {
|
||||
const account = await prisma[Account].findUnique({ where: { compoundId: getCompoundId(providerId, providerAccountId) } })
|
||||
if (!account) { return null }
|
||||
return prisma[User].findUnique({ where: { id: account.userId } })
|
||||
} catch (error) {
|
||||
logger.error('GET_USER_BY_PROVIDER_ACCOUNT_ID_ERROR', error)
|
||||
return Promise.reject(new Error('GET_USER_BY_PROVIDER_ACCOUNT_ID_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function updateUser (user) {
|
||||
debug('UPDATE_USER', user)
|
||||
try {
|
||||
const { id, name, email, image, emailVerified } = user
|
||||
return prisma[User].update({
|
||||
where: { id },
|
||||
data: {
|
||||
name,
|
||||
email,
|
||||
image,
|
||||
emailVerified: emailVerified ? emailVerified.toISOString() : null
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('UPDATE_USER_ERROR', error)
|
||||
return Promise.reject(new Error('UPDATE_USER_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteUser (userId) {
|
||||
debug('DELETE_USER', userId)
|
||||
try {
|
||||
return prisma[User].delete({ where: { id: userId } })
|
||||
} catch (error) {
|
||||
logger.error('DELETE_USER_ERROR', error)
|
||||
return Promise.reject(new Error('DELETE_USER_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function linkAccount (userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires) {
|
||||
debug('LINK_ACCOUNT', userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires)
|
||||
try {
|
||||
return prisma[Account].create({
|
||||
data: {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
compoundId: getCompoundId(providerId, providerAccountId),
|
||||
providerAccountId: `${providerAccountId}`,
|
||||
providerId,
|
||||
providerType,
|
||||
accessTokenExpires,
|
||||
userId
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('LINK_ACCOUNT_ERROR', error)
|
||||
return Promise.reject(new Error('LINK_ACCOUNT_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function unlinkAccount (userId, providerId, providerAccountId) {
|
||||
debug('UNLINK_ACCOUNT', userId, providerId, providerAccountId)
|
||||
try {
|
||||
return prisma[Account].delete({ where: { compoundId: getCompoundId(providerId, providerAccountId) } })
|
||||
} catch (error) {
|
||||
logger.error('UNLINK_ACCOUNT_ERROR', error)
|
||||
return Promise.reject(new Error('UNLINK_ACCOUNT_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function createSession (user) {
|
||||
debug('CREATE_SESSION', user)
|
||||
try {
|
||||
let expires = null
|
||||
if (sessionMaxAge) {
|
||||
const dateExpires = new Date()
|
||||
dateExpires.setTime(dateExpires.getTime() + sessionMaxAge)
|
||||
expires = dateExpires.toISOString()
|
||||
}
|
||||
|
||||
return prisma[Session].create({
|
||||
data: {
|
||||
expires,
|
||||
userId: user.id,
|
||||
sessionToken: randomBytes(32).toString('hex'),
|
||||
accessToken: randomBytes(32).toString('hex')
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('CREATE_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('CREATE_SESSION_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getSession (sessionToken) {
|
||||
debug('GET_SESSION', sessionToken)
|
||||
try {
|
||||
const session = await prisma[Session].findUnique({ where: { sessionToken } })
|
||||
|
||||
// Check session has not expired (do not return it if it has)
|
||||
if (session && session.expires && new Date() > session.expires) {
|
||||
await prisma[Session].delete({ where: { sessionToken } })
|
||||
return null
|
||||
}
|
||||
|
||||
return session
|
||||
} catch (error) {
|
||||
logger.error('GET_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('GET_SESSION_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSession (session, force) {
|
||||
debug('UPDATE_SESSION', session)
|
||||
try {
|
||||
if (sessionMaxAge && (sessionUpdateAge || sessionUpdateAge === 0) && session.expires) {
|
||||
// Calculate last updated date, to throttle write updates to database
|
||||
// Formula: ({expiry date} - sessionMaxAge) + sessionUpdateAge
|
||||
// e.g. ({expiry date} - 30 days) + 1 hour
|
||||
//
|
||||
// Default for sessionMaxAge is 30 days.
|
||||
// Default for sessionUpdateAge is 1 hour.
|
||||
const dateSessionIsDueToBeUpdated = new Date(session.expires)
|
||||
dateSessionIsDueToBeUpdated.setTime(dateSessionIsDueToBeUpdated.getTime() - sessionMaxAge)
|
||||
dateSessionIsDueToBeUpdated.setTime(dateSessionIsDueToBeUpdated.getTime() + sessionUpdateAge)
|
||||
|
||||
// Trigger update of session expiry date and write to database, only
|
||||
// if the session was last updated more than {sessionUpdateAge} ago
|
||||
if (new Date() > dateSessionIsDueToBeUpdated) {
|
||||
const newExpiryDate = new Date()
|
||||
newExpiryDate.setTime(newExpiryDate.getTime() + sessionMaxAge)
|
||||
session.expires = newExpiryDate
|
||||
} else if (!force) {
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
// If session MaxAge, session UpdateAge or session.expires are
|
||||
// missing then don't even try to save changes, unless force is set.
|
||||
if (!force) { return null }
|
||||
}
|
||||
|
||||
const { id, expires } = session
|
||||
return prisma[Session].update({ where: { id }, data: { expires: expires.toISOString() } })
|
||||
} catch (error) {
|
||||
logger.error('UPDATE_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('UPDATE_SESSION_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSession (sessionToken) {
|
||||
debug('DELETE_SESSION', sessionToken)
|
||||
try {
|
||||
return prisma[Session].delete({ where: { sessionToken } })
|
||||
} catch (error) {
|
||||
logger.error('DELETE_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('DELETE_SESSION_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function createVerificationRequest (identifier, url, token, secret, provider) {
|
||||
debug('CREATE_VERIFICATION_REQUEST', identifier)
|
||||
try {
|
||||
const { baseUrl } = appOptions
|
||||
const { sendVerificationRequest, maxAge } = provider
|
||||
|
||||
// Store hashed token (using secret as salt) so that tokens cannot be exploited
|
||||
// even if the contents of the database is compromised.
|
||||
// @TODO Use bcrypt function here instead of simple salted hash
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
|
||||
let expires = null
|
||||
if (maxAge) {
|
||||
const dateExpires = new Date()
|
||||
dateExpires.setTime(dateExpires.getTime() + (maxAge * 1000))
|
||||
expires = dateExpires.toISOString()
|
||||
}
|
||||
|
||||
// Save to database
|
||||
const verificationRequest = await prisma[VerificationRequest].create({
|
||||
data: {
|
||||
identifier,
|
||||
token: hashedToken,
|
||||
expires
|
||||
}
|
||||
})
|
||||
|
||||
// With the verificationCallback on a provider, you can send an email, or queue
|
||||
// an email to be sent, or perform some other action (e.g. send a text message)
|
||||
await sendVerificationRequest({ identifier, url, token, baseUrl, provider })
|
||||
|
||||
return verificationRequest
|
||||
} catch (error) {
|
||||
logger.error('CREATE_VERIFICATION_REQUEST_ERROR', error)
|
||||
return Promise.reject(new Error('CREATE_VERIFICATION_REQUEST_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getVerificationRequest (identifier, token, secret, provider) {
|
||||
debug('GET_VERIFICATION_REQUEST', identifier, token)
|
||||
try {
|
||||
// Hash token provided with secret before trying to match it with database
|
||||
// @TODO Use bcrypt instead of salted SHA-256 hash for token
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
const verificationRequest = await prisma[VerificationRequest].findFirst({
|
||||
where: {
|
||||
identifier,
|
||||
token: hashedToken
|
||||
}
|
||||
})
|
||||
if (verificationRequest && verificationRequest.expires && new Date() > verificationRequest.expires) {
|
||||
// Delete verification entry so it cannot be used again
|
||||
await prisma[VerificationRequest].deleteMany({ where: { identifier, token: hashedToken } })
|
||||
return null
|
||||
}
|
||||
|
||||
return verificationRequest
|
||||
} catch (error) {
|
||||
logger.error('GET_VERIFICATION_REQUEST_ERROR', error)
|
||||
return Promise.reject(new Error('GET_VERIFICATION_REQUEST_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteVerificationRequest (identifier, token, secret, provider) {
|
||||
debug('DELETE_VERIFICATION', identifier, token)
|
||||
try {
|
||||
// Delete verification entry so it cannot be used again
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
await prisma[VerificationRequest].deleteMany({ where: { identifier, token: hashedToken } })
|
||||
} catch (error) {
|
||||
logger.error('DELETE_VERIFICATION_REQUEST_ERROR', error)
|
||||
return Promise.reject(new Error('DELETE_VERIFICATION_REQUEST_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
createUser,
|
||||
getUser,
|
||||
getUserByEmail,
|
||||
getUserByProviderAccountId,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
linkAccount,
|
||||
unlinkAccount,
|
||||
createSession,
|
||||
getSession,
|
||||
updateSession,
|
||||
deleteSession,
|
||||
createVerificationRequest,
|
||||
getVerificationRequest,
|
||||
deleteVerificationRequest
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
getAdapter
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
Adapter
|
||||
}
|
||||
8
src/adapters/typeorm.js
Normal file
8
src/adapters/typeorm.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
* Source code is now at:
|
||||
* https://github.com/nextauthjs/adapters/tree/canary/packages/typeorm-legacy
|
||||
*/
|
||||
|
||||
import TypeORMLegacyAdapter from "@next-auth/typeorm-legacy-adapter"
|
||||
|
||||
export default TypeORMLegacyAdapter
|
||||
@@ -1,384 +0,0 @@
|
||||
import { createConnection, getConnection } from 'typeorm'
|
||||
import { createHash } from 'crypto'
|
||||
import require_optional from 'require_optional' // eslint-disable-line camelcase
|
||||
|
||||
import { CreateUserError } from '../../lib/errors'
|
||||
import adapterConfig from './lib/config'
|
||||
import adapterTransform from './lib/transform'
|
||||
import Models from './models'
|
||||
|
||||
import { updateConnectionEntities } from './lib/utils'
|
||||
|
||||
const Adapter = (typeOrmConfig, options = {}) => {
|
||||
// Ensure typeOrmConfigObject is normalized to an object
|
||||
const typeOrmConfigObject = (typeof typeOrmConfig === 'string')
|
||||
? adapterConfig.parseConnectionString(typeOrmConfig)
|
||||
: typeOrmConfig
|
||||
|
||||
// Load any custom models passed as an option, default to built in models
|
||||
const { models: customModels = {} } = options
|
||||
const models = {
|
||||
User: customModels.User ? customModels.User : Models.User,
|
||||
Account: customModels.Account ? customModels.Account : Models.Account,
|
||||
Session: customModels.Session ? customModels.Session : Models.Session,
|
||||
VerificationRequest: customModels.VerificationRequest ? customModels.VerificationRequest : Models.VerificationRequest
|
||||
}
|
||||
|
||||
// The models are designed for ANSI SQL databases first (as a baseline).
|
||||
// For databases that use a different pragma, we transform the models at run
|
||||
// time *unless* the models are user supplied (in which case we don't do
|
||||
// anything to do them). This function updates arguments by reference.
|
||||
adapterTransform(typeOrmConfigObject, models, options)
|
||||
|
||||
const config = adapterConfig.loadConfig(typeOrmConfigObject, { ...options, models })
|
||||
|
||||
// Create objects from models that can be consumed by functions in the adapter
|
||||
const User = models.User.model
|
||||
const Account = models.Account.model
|
||||
const Session = models.Session.model
|
||||
const VerificationRequest = models.VerificationRequest.model
|
||||
|
||||
let connection = null
|
||||
|
||||
async function getAdapter (appOptions) {
|
||||
const { logger } = appOptions
|
||||
// Display debug output if debug option enabled
|
||||
function debug (debugCode, ...args) {
|
||||
logger.debug(`TYPEORM_${debugCode}`, ...args)
|
||||
}
|
||||
|
||||
// Helper function to reuse / restablish connections
|
||||
// (useful if they drop when after being idle)
|
||||
async function _connect () {
|
||||
// Get current connection by name
|
||||
connection = getConnection(config.name)
|
||||
|
||||
// If connection is no longer established, reconnect
|
||||
if (!connection.isConnected) { connection = await connection.connect() }
|
||||
}
|
||||
|
||||
if (!connection) {
|
||||
// If no connection, create new connection
|
||||
try {
|
||||
connection = await createConnection(config)
|
||||
} catch (error) {
|
||||
if (error.name === 'AlreadyHasActiveConnectionError') {
|
||||
// If creating connection fails because it's already
|
||||
// been re-established, check it's really up
|
||||
await _connect()
|
||||
} else {
|
||||
logger.error('ADAPTER_CONNECTION_ERROR', error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If the connection object already exists, ensure it's valid
|
||||
await _connect()
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
await updateConnectionEntities(connection, config.entities)
|
||||
}
|
||||
|
||||
// Get manager from connection object
|
||||
// https://github.com/typeorm/typeorm/blob/master/docs/entity-manager-api.md
|
||||
const { manager } = connection
|
||||
|
||||
// The models are primarily designed for ANSI SQL database, but some
|
||||
// flexiblity is required in the adapter to support non-SQL databases such
|
||||
// as MongoDB which have different pragmas.
|
||||
//
|
||||
// TypeORM does some abstraction, but doesn't handle everything (e.g. it
|
||||
// handles translating `id` and `_id` in models, but not queries) so we
|
||||
// need to handle somethings in the adapter to make it compatible.
|
||||
let idKey = 'id'
|
||||
let ObjectId
|
||||
if (config.type === 'mongodb') {
|
||||
idKey = '_id'
|
||||
// Using a dynamic import causes problems for some compilers/bundlers
|
||||
// that don't handle dynamic imports. To try and work around this we are
|
||||
// using the same method mongodb uses to load Object ID type, which is to
|
||||
// use the require_optional loader.
|
||||
const mongodb = require_optional('mongodb')
|
||||
ObjectId = mongodb.ObjectId
|
||||
}
|
||||
|
||||
// These values are stored as seconds, but to use them with dates in
|
||||
// JavaScript we convert them to milliseconds.
|
||||
//
|
||||
// Use a conditional to default to 30 day session age if not set - it should
|
||||
// always be set but a meaningful fallback is helpful to facilitate testing.
|
||||
if (appOptions && (!appOptions.session || !appOptions.session.maxAge)) {
|
||||
debug('GET_ADAPTER', 'Session expiry not configured (defaulting to 30 days')
|
||||
}
|
||||
const defaultSessionMaxAge = 30 * 24 * 60 * 60 * 1000
|
||||
const sessionMaxAge = (appOptions && appOptions.session && appOptions.session.maxAge)
|
||||
? appOptions.session.maxAge * 1000
|
||||
: defaultSessionMaxAge
|
||||
const sessionUpdateAge = (appOptions && appOptions.session && appOptions.session.updateAge)
|
||||
? appOptions.session.updateAge * 1000
|
||||
: 0
|
||||
|
||||
async function createUser (profile) {
|
||||
debug('CREATE_USER', profile)
|
||||
try {
|
||||
// Create user account
|
||||
const user = new User(profile.name, profile.email, profile.image, profile.emailVerified)
|
||||
return await manager.save(user)
|
||||
} catch (error) {
|
||||
logger.error('CREATE_USER_ERROR', error)
|
||||
return Promise.reject(new CreateUserError(error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getUser (id) {
|
||||
debug('GET_USER', id)
|
||||
|
||||
// In the very specific case of both using JWT for storing session data
|
||||
// and using MongoDB to store user data, the ID is a string rather than
|
||||
// an ObjectId and we need to turn it into an ObjectId.
|
||||
//
|
||||
// In all other scenarios it is already an ObjectId, because it will have
|
||||
// come from another MongoDB query.
|
||||
if (ObjectId && !(id instanceof ObjectId)) {
|
||||
id = ObjectId(id)
|
||||
}
|
||||
|
||||
try {
|
||||
return manager.findOne(User, { [idKey]: id })
|
||||
} catch (error) {
|
||||
logger.error('GET_USER_BY_ID_ERROR', error)
|
||||
return Promise.reject(new Error('GET_USER_BY_ID_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getUserByEmail (email) {
|
||||
debug('GET_USER_BY_EMAIL', email)
|
||||
try {
|
||||
if (!email) { return Promise.resolve(null) }
|
||||
return manager.findOne(User, { email })
|
||||
} catch (error) {
|
||||
logger.error('GET_USER_BY_EMAIL_ERROR', error)
|
||||
return Promise.reject(new Error('GET_USER_BY_EMAIL_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getUserByProviderAccountId (providerId, providerAccountId) {
|
||||
debug('GET_USER_BY_PROVIDER_ACCOUNT_ID', providerId, providerAccountId)
|
||||
try {
|
||||
const account = await manager.findOne(Account, { providerId, providerAccountId })
|
||||
if (!account) { return null }
|
||||
return manager.findOne(User, { [idKey]: account.userId })
|
||||
} catch (error) {
|
||||
logger.error('GET_USER_BY_PROVIDER_ACCOUNT_ID_ERROR', error)
|
||||
return Promise.reject(new Error('GET_USER_BY_PROVIDER_ACCOUNT_ID_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function updateUser (user) {
|
||||
debug('UPDATE_USER', user)
|
||||
return manager.save(User, user)
|
||||
}
|
||||
|
||||
async function deleteUser (userId) {
|
||||
debug('DELETE_USER', userId)
|
||||
// @TODO Delete user from DB
|
||||
return false
|
||||
}
|
||||
|
||||
async function linkAccount (userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires) {
|
||||
debug('LINK_ACCOUNT', userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires)
|
||||
try {
|
||||
// Create provider account linked to user
|
||||
const account = new Account(userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires)
|
||||
return manager.save(account)
|
||||
} catch (error) {
|
||||
logger.error('LINK_ACCOUNT_ERROR', error)
|
||||
return Promise.reject(new Error('LINK_ACCOUNT_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function unlinkAccount (userId, providerId, providerAccountId) {
|
||||
debug('UNLINK_ACCOUNT', userId, providerId, providerAccountId)
|
||||
// @TODO Get current user from DB
|
||||
// @TODO Delete [provider] object from user object
|
||||
// @TODO Save changes to user object in DB
|
||||
return false
|
||||
}
|
||||
|
||||
async function createSession (user) {
|
||||
debug('CREATE_SESSION', user)
|
||||
try {
|
||||
let expires = null
|
||||
if (sessionMaxAge) {
|
||||
const dateExpires = new Date()
|
||||
dateExpires.setTime(dateExpires.getTime() + sessionMaxAge)
|
||||
expires = dateExpires
|
||||
}
|
||||
|
||||
const session = new Session(user.id, expires)
|
||||
|
||||
return manager.save(session)
|
||||
} catch (error) {
|
||||
logger.error('CREATE_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('CREATE_SESSION_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getSession (sessionToken) {
|
||||
debug('GET_SESSION', sessionToken)
|
||||
try {
|
||||
const session = await manager.findOne(Session, { sessionToken })
|
||||
|
||||
// Check session has not expired (do not return it if it has)
|
||||
if (session && session.expires && new Date() > new Date(session.expires)) {
|
||||
// @TODO Delete old sessions from database
|
||||
return null
|
||||
}
|
||||
|
||||
return session
|
||||
} catch (error) {
|
||||
logger.error('GET_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('GET_SESSION_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSession (session, force) {
|
||||
debug('UPDATE_SESSION', session)
|
||||
try {
|
||||
if (sessionMaxAge && (sessionUpdateAge || sessionUpdateAge === 0) && session.expires) {
|
||||
// Calculate last updated date, to throttle write updates to database
|
||||
// Formula: ({expiry date} - sessionMaxAge) + sessionUpdateAge
|
||||
// e.g. ({expiry date} - 30 days) + 1 hour
|
||||
//
|
||||
// Default for sessionMaxAge is 30 days.
|
||||
// Default for sessionUpdateAge is 1 hour.
|
||||
const dateSessionIsDueToBeUpdated = new Date(session.expires)
|
||||
dateSessionIsDueToBeUpdated.setTime(dateSessionIsDueToBeUpdated.getTime() - sessionMaxAge)
|
||||
dateSessionIsDueToBeUpdated.setTime(dateSessionIsDueToBeUpdated.getTime() + sessionUpdateAge)
|
||||
|
||||
// Trigger update of session expiry date and write to database, only
|
||||
// if the session was last updated more than {sessionUpdateAge} ago
|
||||
if (new Date() > dateSessionIsDueToBeUpdated) {
|
||||
const newExpiryDate = new Date()
|
||||
newExpiryDate.setTime(newExpiryDate.getTime() + sessionMaxAge)
|
||||
session.expires = newExpiryDate
|
||||
} else if (!force) {
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
// If session MaxAge, session UpdateAge or session.expires are
|
||||
// missing then don't even try to save changes, unless force is set.
|
||||
if (!force) { return null }
|
||||
}
|
||||
|
||||
return manager.save(Session, session)
|
||||
} catch (error) {
|
||||
logger.error('UPDATE_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('UPDATE_SESSION_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSession (sessionToken) {
|
||||
debug('DELETE_SESSION', sessionToken)
|
||||
try {
|
||||
return await manager.delete(Session, { sessionToken })
|
||||
} catch (error) {
|
||||
logger.error('DELETE_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('DELETE_SESSION_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function createVerificationRequest (identifier, url, token, secret, provider) {
|
||||
debug('CREATE_VERIFICATION_REQUEST', identifier)
|
||||
try {
|
||||
const { baseUrl } = appOptions
|
||||
const { sendVerificationRequest, maxAge } = provider
|
||||
|
||||
// Store hashed token (using secret as salt) so that tokens cannot be exploited
|
||||
// even if the contents of the database is compromised.
|
||||
// @TODO Use bcrypt function here instead of simple salted hash
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
|
||||
let expires = null
|
||||
if (maxAge) {
|
||||
const dateExpires = new Date()
|
||||
dateExpires.setTime(dateExpires.getTime() + (maxAge * 1000))
|
||||
expires = dateExpires
|
||||
}
|
||||
|
||||
// Save to database
|
||||
const newVerificationRequest = new VerificationRequest(identifier, hashedToken, expires)
|
||||
const verificationRequest = await manager.save(newVerificationRequest)
|
||||
|
||||
// With the verificationCallback on a provider, you can send an email, or queue
|
||||
// an email to be sent, or perform some other action (e.g. send a text message)
|
||||
await sendVerificationRequest({ identifier, url, token, baseUrl, provider })
|
||||
|
||||
return verificationRequest
|
||||
} catch (error) {
|
||||
logger.error('CREATE_VERIFICATION_REQUEST_ERROR', error)
|
||||
return Promise.reject(new Error('CREATE_VERIFICATION_REQUEST_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getVerificationRequest (identifier, token, secret, provider) {
|
||||
debug('GET_VERIFICATION_REQUEST', identifier, token)
|
||||
try {
|
||||
// Hash token provided with secret before trying to match it with database
|
||||
// @TODO Use bcrypt instead of salted SHA-256 hash for token
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
const verificationRequest = await manager.findOne(VerificationRequest, { identifier, token: hashedToken })
|
||||
|
||||
if (verificationRequest && verificationRequest.expires && new Date() > new Date(verificationRequest.expires)) {
|
||||
// Delete verification entry so it cannot be used again
|
||||
await manager.delete(VerificationRequest, { identifier, token: hashedToken })
|
||||
return null
|
||||
}
|
||||
|
||||
return verificationRequest
|
||||
} catch (error) {
|
||||
logger.error('GET_VERIFICATION_REQUEST_ERROR', error)
|
||||
return Promise.reject(new Error('GET_VERIFICATION_REQUEST_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteVerificationRequest (identifier, token, secret, provider) {
|
||||
debug('DELETE_VERIFICATION', identifier, token)
|
||||
try {
|
||||
// Delete verification entry so it cannot be used again
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
await manager.delete(VerificationRequest, { identifier, token: hashedToken })
|
||||
} catch (error) {
|
||||
logger.error('DELETE_VERIFICATION_REQUEST_ERROR', error)
|
||||
return Promise.reject(new Error('DELETE_VERIFICATION_REQUEST_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
createUser,
|
||||
getUser,
|
||||
getUserByEmail,
|
||||
getUserByProviderAccountId,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
linkAccount,
|
||||
unlinkAccount,
|
||||
createSession,
|
||||
getSession,
|
||||
updateSession,
|
||||
deleteSession,
|
||||
createVerificationRequest,
|
||||
getVerificationRequest,
|
||||
deleteVerificationRequest
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
getAdapter
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
Adapter,
|
||||
Models
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import { EntitySchema } from 'typeorm'
|
||||
|
||||
const parseConnectionString = (configString) => {
|
||||
if (typeof configString !== 'string') { return configString }
|
||||
|
||||
// If the input is URL string, automatically convert the string to an object
|
||||
// to make configuration easier (in most use cases).
|
||||
//
|
||||
// TypeORM accepts connection string as a 'url' option, but unfortunately
|
||||
// not for all databases (e.g. SQLite) or for all options, so we handle
|
||||
// parsing it in this function.
|
||||
try {
|
||||
const parsedUrl = new URL(configString)
|
||||
const config = {}
|
||||
|
||||
if (parsedUrl.protocol.startsWith('mongodb+srv')) {
|
||||
// Special case handling is required for mongodb+srv with TypeORM
|
||||
config.type = 'mongodb'
|
||||
config.url = configString.replace(/\?(.*)$/, '')
|
||||
config.useNewUrlParser = true
|
||||
} else {
|
||||
config.type = parsedUrl.protocol.replace(/:$/, '')
|
||||
config.host = parsedUrl.hostname
|
||||
config.port = Number(parsedUrl.port)
|
||||
config.username = parsedUrl.username
|
||||
config.password = parsedUrl.password
|
||||
config.database = parsedUrl.pathname.replace(/^\//, '').replace(/\?(.*)$/, '')
|
||||
config.options = {}
|
||||
}
|
||||
|
||||
// This option is recommended by mongodb
|
||||
if (config.type === 'mongodb') {
|
||||
config.useUnifiedTopology = true
|
||||
}
|
||||
|
||||
// Prevents warning about deprecated option (sets default value)
|
||||
if (config.type === 'mssql') {
|
||||
config.options.enableArithAbort = true
|
||||
}
|
||||
|
||||
if (parsedUrl.search) {
|
||||
parsedUrl.search.replace(/^\?/, '').split('&').forEach(keyValuePair => {
|
||||
let [key, value] = keyValuePair.split('=')
|
||||
// Converts true/false strings to actual boolean values
|
||||
if (value === 'true') { value = true }
|
||||
if (value === 'false') { value = false }
|
||||
config[key] = value
|
||||
})
|
||||
}
|
||||
|
||||
return config
|
||||
} catch (error) {
|
||||
// If URL parsing fails for any reason, try letting TypeORM handle it
|
||||
return {
|
||||
url: configString
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const loadConfig = (config, { models, namingStrategy }) => {
|
||||
const defaultConfig = {
|
||||
name: 'nextauth',
|
||||
autoLoadEntities: true,
|
||||
entities: [
|
||||
new EntitySchema(models.User.schema),
|
||||
new EntitySchema(models.Account.schema),
|
||||
new EntitySchema(models.Session.schema),
|
||||
new EntitySchema(models.VerificationRequest.schema)
|
||||
],
|
||||
timezone: 'Z', // Required for timestamps to be treated as UTC in MySQL
|
||||
logging: false,
|
||||
namingStrategy
|
||||
}
|
||||
|
||||
return {
|
||||
...defaultConfig,
|
||||
...config
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
parseConnectionString,
|
||||
loadConfig
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
// Inspired by https://github.com/tonivj5/typeorm-naming-strategies
|
||||
import { DefaultNamingStrategy } from 'typeorm'
|
||||
import { snakeCase, camelCase } from 'typeorm/util/StringUtils'
|
||||
|
||||
export class SnakeCaseNamingStrategy extends DefaultNamingStrategy {
|
||||
// Pluralise table names (set customName to override)
|
||||
tableName (className, customName) {
|
||||
return customName || snakeCase(`${className}s`)
|
||||
}
|
||||
|
||||
columnName (propertyName, customName, embeddedPrefixes) {
|
||||
return `${snakeCase(embeddedPrefixes.join('_'))}${customName || snakeCase(propertyName)}`
|
||||
}
|
||||
|
||||
relationName (propertyName) {
|
||||
return snakeCase(propertyName)
|
||||
}
|
||||
|
||||
joinColumnName (relationName, referencedColumnName) {
|
||||
return snakeCase(`${relationName}_${referencedColumnName}`)
|
||||
}
|
||||
|
||||
joinTableName (firstTableName, secondTableName, firstPropertyName, secondPropertyName) {
|
||||
return snakeCase(`${firstTableName}_${firstPropertyName.replace(/\./gi, '_')}_${secondTableName}`)
|
||||
}
|
||||
|
||||
joinTableColumnName (tableName, propertyName, columnName) {
|
||||
return snakeCase(`${tableName}_${(columnName || propertyName)}`)
|
||||
}
|
||||
|
||||
classTableInheritanceParentColumnName (parentTableName, parentTableIdPropertyName) {
|
||||
return snakeCase(`${parentTableName}_${parentTableIdPropertyName}`)
|
||||
}
|
||||
|
||||
eagerJoinRelationAlias (alias, propertyPath) {
|
||||
return `${alias}__${propertyPath.replace('.', '_')}`
|
||||
}
|
||||
}
|
||||
|
||||
export class CamelCaseNamingStrategy extends DefaultNamingStrategy {
|
||||
// Pluralise collection names, uses (set customName to override)
|
||||
tableName (className, customName) {
|
||||
return customName || camelCase(`${className}s`)
|
||||
}
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
// Perform transforms on SQL models so they can be used with other databases
|
||||
import { SnakeCaseNamingStrategy, CamelCaseNamingStrategy } from './naming-strategies'
|
||||
|
||||
const postgresTransform = (models, options) => {
|
||||
// Apply snake case naming strategy for Postgres databases
|
||||
if (!options.namingStrategy) {
|
||||
options.namingStrategy = new SnakeCaseNamingStrategy()
|
||||
}
|
||||
|
||||
// For Postgres we need to use the `timestamp with time zone` type
|
||||
// aka `timestamptz` to store timestamps correctly in UTC.
|
||||
for (const model in models) {
|
||||
for (const column in models[model].schema.columns) {
|
||||
if (models[model].schema.columns[column].type === 'timestamp') {
|
||||
models[model].schema.columns[column].type = 'timestamptz'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mysqlTransform = (models, options) => {
|
||||
// Apply snake case naming strategy for MySQL databases
|
||||
if (!options.namingStrategy) {
|
||||
options.namingStrategy = new SnakeCaseNamingStrategy()
|
||||
}
|
||||
|
||||
// For MySQL we default milisecond precision of all timestamps to 6 digits.
|
||||
// This ensures all timestamp fields use the same precision (unless explictly
|
||||
// configured otherwise) and that values in MySQL match those Postgress.
|
||||
for (const model in models) {
|
||||
for (const column in models[model].schema.columns) {
|
||||
if (models[model].schema.columns[column].type === 'timestamp') {
|
||||
// If precision explictly set (including to null) don't change it
|
||||
if (typeof models[model].schema.columns[column].precision === 'undefined') {
|
||||
models[model].schema.columns[column].precision = 6
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mongodbTransform = (models, options) => {
|
||||
// A CamelCase naming strategy is used for all document databases
|
||||
if (!options.namingStrategy) {
|
||||
options.namingStrategy = new CamelCaseNamingStrategy()
|
||||
}
|
||||
|
||||
// Important!
|
||||
//
|
||||
// 1. You must set 'objectId: true' on one property on a model in MongoDB.
|
||||
//
|
||||
// 'objectId' MUST be set on the primary ID field. This overrides other
|
||||
// values on that object in TypeORM (e.g. type: 'int' or 'primary').
|
||||
//
|
||||
// 2. Other properties that are Object IDs in the same model MUST be set to
|
||||
// type: 'objectId' (and should not be set to `objectId: true`).
|
||||
//
|
||||
// If you set 'objectId: true' on multiple properties on a model you will
|
||||
// see the result of queries like find() is wrong. You will see the same
|
||||
// Object ID in every property of type Object ID in the result (but the
|
||||
// database will look fine); so use `type: 'objectId'` for them instead.
|
||||
for (const model in models) {
|
||||
delete models[model].schema.columns.id.type
|
||||
models[model].schema.columns.id.objectId = true
|
||||
}
|
||||
|
||||
// Ensure reference to User ID in other models are Object IDs
|
||||
// This needs to done for any properties that reference another entity by ID
|
||||
models.Account.schema.columns.userId.type = 'objectId'
|
||||
models.Session.schema.columns.userId.type = 'objectId'
|
||||
|
||||
// The options `unique: true` and `nullable: true` don't work the same
|
||||
// with MongoDB as they do with SQL databases like MySQL and Postgres,
|
||||
// we need to create a sparse index to only allow unique values, while
|
||||
// still allowing multiple entires to omit the email address.
|
||||
delete models.User.schema.columns.email.unique
|
||||
|
||||
if (!models.User.schema.indices) { models.User.schema.indices = [] }
|
||||
|
||||
models.User.schema.indices.push({
|
||||
name: 'email',
|
||||
unique: true,
|
||||
sparse: true,
|
||||
columns: ['email']
|
||||
})
|
||||
}
|
||||
|
||||
const sqliteTransform = (models, options) => {
|
||||
// Apply snake case naming strategy for SQLite databases
|
||||
if (!options.namingStrategy) {
|
||||
options.namingStrategy = new SnakeCaseNamingStrategy()
|
||||
}
|
||||
|
||||
// SQLite does not support `timestamp` fields so we remap them to `datetime`
|
||||
// in all models.
|
||||
//
|
||||
// `timestamp` is an ANSI SQL specification and widely supported by other
|
||||
// databases so this transform is a specific workaround required for SQLite.
|
||||
//
|
||||
// NB: SQLite adds 'create' and 'update' fields to allow rows, but that is
|
||||
// specific to SQLite and so we ignore that behaviour.
|
||||
for (const model in models) {
|
||||
for (const column in models[model].schema.columns) {
|
||||
if (models[model].schema.columns[column].type === 'timestamp') {
|
||||
models[model].schema.columns[column].type = 'datetime'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mssqlTransform = (models, options) => {
|
||||
// Apply snake case naming strategy for SQL Server databases
|
||||
if (!options.namingStrategy) {
|
||||
// @TODO Add TitleCase instead as more common MSSQL convention?
|
||||
options.namingStrategy = new SnakeCaseNamingStrategy()
|
||||
}
|
||||
|
||||
// SQL Server deprecated TIMESTAMP in favor of ROWVERSION.
|
||||
// But ROWVERSION is not what it was intended in the other adapters.
|
||||
for (const model in models) {
|
||||
for (const column in models[model].schema.columns) {
|
||||
if (models[model].schema.columns[column].type === 'timestamp') {
|
||||
models[model].schema.columns[column].type = 'datetime'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Support UNIQUE on on User.email that allows duplicate NULL values
|
||||
// Note: This is ANSI SQL behaviour for UNIQUE not default in SQL Server
|
||||
delete models.User.schema.columns.email.unique
|
||||
|
||||
if (!models.User.schema.indices) { models.User.schema.indices = [] }
|
||||
|
||||
models.User.schema.indices.push({
|
||||
name: 'email',
|
||||
columns: ['email'],
|
||||
unique: true,
|
||||
where: 'email IS NOT NULL'
|
||||
})
|
||||
}
|
||||
|
||||
export default (config, models, options) => {
|
||||
// @TODO Refactor into switch statement
|
||||
if ((config.type && config.type.startsWith('mongodb')) ||
|
||||
(config.url && config.url.startsWith('mongodb'))) {
|
||||
mongodbTransform(models, options)
|
||||
} else if ((config.type && config.type.startsWith('postgres')) ||
|
||||
(config.url && config.url.startsWith('postgres'))) {
|
||||
postgresTransform(models, options)
|
||||
} else if ((config.type && config.type.startsWith('mysql')) ||
|
||||
(config.url && config.url.startsWith('mysql'))) {
|
||||
mysqlTransform(models, options)
|
||||
} else if ((config.type && config.type.startsWith('sqlite')) ||
|
||||
(config.url && config.url.startsWith('sqlite'))) {
|
||||
sqliteTransform(models, options)
|
||||
} else if ((config.type && config.type.startsWith('mssql')) ||
|
||||
(config.url && config.url.startsWith('mssql'))) {
|
||||
mssqlTransform(models, options)
|
||||
} else {
|
||||
// For all other SQL databases (e.g. MySQL) apply snake case naming
|
||||
// strategy, but otherwise use the models and schemas as they are.
|
||||
if (!options.namingStrategy) {
|
||||
options.namingStrategy = new SnakeCaseNamingStrategy()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
const entitiesChanged = (prevEntities, newEntities) => {
|
||||
if (prevEntities.length !== newEntities.length) return true
|
||||
for (let i = 0; i < prevEntities.length; i++) {
|
||||
if (prevEntities[i] !== newEntities[i]) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export const updateConnectionEntities = async (connection, entities) => {
|
||||
// Check if the entities passed have changed and if so replace them
|
||||
// and re-sync the typeorm connection.
|
||||
if (!connection || !entitiesChanged(connection.options.entities, entities)) return
|
||||
connection.options.entities = entities
|
||||
connection.buildMetadatas()
|
||||
if (connection.options.synchronize) {
|
||||
await connection.synchronize()
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import { createHash } from 'crypto'
|
||||
|
||||
export class Account {
|
||||
constructor (
|
||||
userId,
|
||||
providerId,
|
||||
providerType,
|
||||
providerAccountId,
|
||||
refreshToken,
|
||||
accessToken,
|
||||
accessTokenExpires
|
||||
) {
|
||||
// The compound ID ensures there is only one entry for a given provider and account
|
||||
this.compoundId = createHash('sha256').update(`${providerId}:${providerAccountId}`).digest('hex')
|
||||
this.userId = userId
|
||||
this.providerType = providerType
|
||||
this.providerId = providerId
|
||||
this.providerAccountId = providerAccountId
|
||||
this.refreshToken = refreshToken
|
||||
this.accessToken = accessToken
|
||||
this.accessTokenExpires = accessTokenExpires
|
||||
}
|
||||
}
|
||||
|
||||
export const AccountSchema = {
|
||||
name: 'Account',
|
||||
target: Account,
|
||||
columns: {
|
||||
id: {
|
||||
// This property has `objectId: true` instead of `type: int` in MongoDB
|
||||
primary: true,
|
||||
type: 'int',
|
||||
generated: true
|
||||
},
|
||||
compoundId: {
|
||||
// The compound ID ensures that there there is only one instance of an
|
||||
// OAuth account in a way that works across different databases.
|
||||
// It is not used for anything else.
|
||||
type: 'varchar',
|
||||
unique: true
|
||||
},
|
||||
userId: {
|
||||
// This property is set to `type: objectId` on MongoDB databases
|
||||
type: 'int'
|
||||
},
|
||||
providerType: {
|
||||
type: 'varchar'
|
||||
},
|
||||
providerId: {
|
||||
type: 'varchar'
|
||||
},
|
||||
providerAccountId: {
|
||||
type: 'varchar'
|
||||
},
|
||||
refreshToken: {
|
||||
type: 'text',
|
||||
nullable: true
|
||||
},
|
||||
accessToken: {
|
||||
// AccessTokens are not (yet) automatically rotated by NextAuth.js
|
||||
// You can update it using the refreshToken and the accessTokenUrl endpoint for the provider
|
||||
type: 'text',
|
||||
nullable: true
|
||||
},
|
||||
accessTokenExpires: {
|
||||
// AccessTokens expiry times are not (yet) updated by NextAuth.js
|
||||
// You can update it using the refreshToken and the accessTokenUrl endpoint for the provider
|
||||
type: 'timestamp',
|
||||
nullable: true
|
||||
},
|
||||
createdAt: {
|
||||
type: 'timestamp',
|
||||
createDate: true
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'timestamp',
|
||||
updateDate: true
|
||||
}
|
||||
},
|
||||
indices: [
|
||||
{
|
||||
name: 'userId',
|
||||
columns: ['userId']
|
||||
},
|
||||
{
|
||||
name: 'providerId',
|
||||
columns: ['providerId']
|
||||
},
|
||||
{
|
||||
name: 'providerAccountId',
|
||||
columns: ['providerAccountId']
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Account, AccountSchema } from './account'
|
||||
import { User, UserSchema } from './user'
|
||||
import { Session, SessionSchema } from './session'
|
||||
import { VerificationRequest, VerificationRequestSchema } from './verification-request'
|
||||
|
||||
export default {
|
||||
Account: {
|
||||
model: Account,
|
||||
schema: AccountSchema
|
||||
},
|
||||
User: {
|
||||
model: User,
|
||||
schema: UserSchema
|
||||
},
|
||||
Session: {
|
||||
model: Session,
|
||||
schema: SessionSchema
|
||||
},
|
||||
VerificationRequest: {
|
||||
model: VerificationRequest,
|
||||
schema: VerificationRequestSchema
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { randomBytes } from 'crypto'
|
||||
|
||||
export class Session {
|
||||
constructor (userId, expires, sessionToken, accessToken) {
|
||||
this.userId = userId
|
||||
this.expires = expires
|
||||
this.sessionToken = sessionToken || randomBytes(32).toString('hex')
|
||||
this.accessToken = accessToken || randomBytes(32).toString('hex')
|
||||
}
|
||||
}
|
||||
|
||||
export const SessionSchema = {
|
||||
name: 'Session',
|
||||
target: Session,
|
||||
columns: {
|
||||
id: {
|
||||
// This property has `objectId: true` instead of `type: int` in MongoDB
|
||||
primary: true,
|
||||
type: 'int',
|
||||
generated: true
|
||||
},
|
||||
userId: {
|
||||
// This property is set to `type: objectId` on MongoDB databases
|
||||
type: 'int'
|
||||
},
|
||||
expires: {
|
||||
// The date the session expires (is updated when a session is active)
|
||||
type: 'timestamp'
|
||||
},
|
||||
sessionToken: {
|
||||
// The sessionToken should never be exposed to client side JavaScript
|
||||
type: 'varchar',
|
||||
unique: true
|
||||
},
|
||||
accessToken: {
|
||||
// The accessToken can be safely exposed to client side JavaScript to
|
||||
// to identify the owner of a session without exposing the sessionToken
|
||||
type: 'varchar',
|
||||
unique: true
|
||||
},
|
||||
createdAt: {
|
||||
type: 'timestamp',
|
||||
createDate: true
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'timestamp',
|
||||
updateDate: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
export class User {
|
||||
constructor (name, email, image, emailVerified) {
|
||||
if (name) { this.name = name }
|
||||
if (email) { this.email = email }
|
||||
if (image) { this.image = image }
|
||||
if (emailVerified) {
|
||||
const currentDate = new Date()
|
||||
this.emailVerified = currentDate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const UserSchema = {
|
||||
name: 'User',
|
||||
target: User,
|
||||
columns: {
|
||||
id: {
|
||||
// This property has `objectId: true` instead of `type: int` in MongoDB
|
||||
primary: true,
|
||||
type: 'int',
|
||||
generated: true
|
||||
},
|
||||
name: {
|
||||
type: 'varchar',
|
||||
nullable: true
|
||||
},
|
||||
email: {
|
||||
// This is inherited from the one in the OAuth provider profile on
|
||||
// initial sign in, if one is specified in that profile.
|
||||
type: 'varchar',
|
||||
unique: true,
|
||||
nullable: true
|
||||
},
|
||||
emailVerified: {
|
||||
// Contains a timestamp of the last time an action was performed that
|
||||
// confirmed this email address was active and used by the user (e.g.
|
||||
// when an email sign in link is clicked on and verified). Is null
|
||||
// if the email address specified has never been verified.
|
||||
type: 'timestamp',
|
||||
nullable: true
|
||||
},
|
||||
image: {
|
||||
// A URL that points to an avatar to use for the user.
|
||||
// This is inherited from the one in the OAuth provider profile on
|
||||
// initial sign in, if one is specified in that profile.
|
||||
type: 'varchar',
|
||||
nullable: true
|
||||
},
|
||||
createdAt: {
|
||||
type: 'timestamp',
|
||||
createDate: true
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'timestamp',
|
||||
updateDate: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
// This model is used for sign in emails, but is designed to support other
|
||||
// mechanisms in future (e.g. 2FA via text message or short codes)
|
||||
export class VerificationRequest {
|
||||
constructor (identifier, token, expires) {
|
||||
if (identifier) { this.identifier = identifier }
|
||||
if (token) { this.token = token }
|
||||
if (expires) { this.expires = expires }
|
||||
}
|
||||
}
|
||||
|
||||
export const VerificationRequestSchema = {
|
||||
name: 'VerificationRequest',
|
||||
target: VerificationRequest,
|
||||
columns: {
|
||||
id: {
|
||||
// This property has `objectId: true` instead of `type: int` in MongoDB
|
||||
primary: true,
|
||||
type: 'int',
|
||||
generated: true
|
||||
},
|
||||
identifier: {
|
||||
// An email address, phone number, username or other unique identifier
|
||||
// associated with the request (used to track who it was on behalf of)
|
||||
type: 'varchar'
|
||||
},
|
||||
token: {
|
||||
// The token used verify the request (maybe hashed or encrypted)
|
||||
type: 'varchar',
|
||||
unique: true
|
||||
},
|
||||
expires: {
|
||||
// After this time, the request will no longer ve valid
|
||||
type: 'timestamp'
|
||||
},
|
||||
createdAt: {
|
||||
type: 'timestamp',
|
||||
createDate: true
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'timestamp',
|
||||
updateDate: true
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/providers/42.js
Normal file
20
src/providers/42.js
Normal file
@@ -0,0 +1,20 @@
|
||||
export default function FortyTwo(options) {
|
||||
return {
|
||||
id: '42-school',
|
||||
name: '42 School',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: 'https://api.intra.42.fr/oauth/token',
|
||||
authorizationUrl:
|
||||
'https://api.intra.42.fr/oauth/authorize?response_type=code',
|
||||
profileUrl: 'https://api.intra.42.fr/v2/me',
|
||||
profile: (profile) => ({
|
||||
id: profile.id,
|
||||
email: profile.email,
|
||||
image: profile.image_url,
|
||||
name: profile.usual_full_name,
|
||||
}),
|
||||
...options,
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
# To be able to run tests:
|
||||
# 1. copy to the root folder and rename to .env
|
||||
# 2. Populate with values
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
NEXTAUTH_TWITTER_ID=
|
||||
NEXTAUTH_TWITTER_SECRET=
|
||||
NEXTAUTH_TWITTER_USERNAME=
|
||||
NEXTAUTH_TWITTER_PASSWORD=
|
||||
NEXTAUTH_GITHUB_ID=
|
||||
NEXTAUTH_GITHUB_SECRET=
|
||||
NEXTAUTH_GITHUB_USERNAME=
|
||||
NEXTAUTH_GITHUB_PASSWORD=
|
||||
NEXTAUTH_GOOGLE_ID=
|
||||
NEXTAUTH_GOOGLE_SECRET=
|
||||
NEXTAUTH_GOOGLE_USERNAME=
|
||||
NEXTAUTH_GOOGLE_PASSWORD=
|
||||
@@ -1,30 +0,0 @@
|
||||
# Multi stage build to allow us to improve performance
|
||||
FROM node:10-alpine as base
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Install basic dependancies (Next.js, React)
|
||||
COPY test/docker/app/package*.json ./
|
||||
RUN npm ci --only=production
|
||||
|
||||
FROM node:10-alpine as app
|
||||
COPY --from=base /usr/src/app ./
|
||||
|
||||
# Copy last build of library into the image and install dependences for it.
|
||||
# This ensures the build is valid and package.json contains everything needed
|
||||
# to actually run the library.
|
||||
# Note: You must run `npm run build` first to build a release of the library
|
||||
RUN mkdir -p node_modules/next-auth
|
||||
# Copy all entrypoints for the library (if creating a new one, add it here)
|
||||
COPY index.js providers.js adapters.js client.js jwt.js node_modules/next-auth/
|
||||
# Copy the dist dir
|
||||
COPY dist node_modules/next-auth/dist
|
||||
# Copy the package.json for the library and install it's dependences
|
||||
COPY package*.json node_modules/next-auth/
|
||||
RUN cd node_modules/next-auth/ && npm ci --only=production
|
||||
|
||||
# Copy test pages across
|
||||
COPY test/docker/app/pages ./pages
|
||||
|
||||
RUN npm run build
|
||||
|
||||
CMD [ "npm", "start" ]
|
||||
@@ -1,52 +0,0 @@
|
||||
# Start test app with local databases inside the container.
|
||||
#
|
||||
# Note: Uses Docker Compose v2 as v3 doesn't currently support extends.
|
||||
# https://docs.docker.com/compose/compose-file/compose-file-v2/
|
||||
version: "2.3"
|
||||
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: ../../
|
||||
dockerfile: ./test/Dockerfile
|
||||
environment:
|
||||
# Set env vars in your current terminal or in .env in the root directory
|
||||
- NEXTAUTH_URL=${NEXTAUTH_URL}
|
||||
- NEXTAUTH_DATABASE_URL=${NEXTAUTH_DATABASE_URL}
|
||||
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
|
||||
- NEXTAUTH_JWT_SESSIONS=${NEXTAUTH_JWT_SESSIONS}
|
||||
- NEXTAUTH_AUTH0_ID=${NEXTAUTH_AUTH0_ID}
|
||||
- NEXTAUTH_AUTH0_SECRET=${NEXTAUTH_AUTH0_SECRET}
|
||||
- NEXTAUTH_AUTH0_DOMAIN=${NEXTAUTH_AUTH0_DOMAIN}
|
||||
- NEXTAUTH_FACEBOOK_ID=${NEXTAUTH_FACEBOOK_ID}
|
||||
- NEXTAUTH_FACEBOOK_SECRET=${NEXTAUTH_FACEBOOK_SECRET}
|
||||
- NEXTAUTH_GITHUB_ID=${NEXTAUTH_GITHUB_ID}
|
||||
- NEXTAUTH_GITHUB_SECRET=${NEXTAUTH_GITHUB_SECRET}
|
||||
- NEXTAUTH_GOOGLE_ID=${NEXTAUTH_GOOGLE_ID}
|
||||
- NEXTAUTH_GOOGLE_SECRET=${NEXTAUTH_GOOGLE_SECRET}
|
||||
- NEXTAUTH_TWITTER_ID=${NEXTAUTH_TWITTER_ID}
|
||||
- NEXTAUTH_TWITTER_SECRET=${NEXTAUTH_TWITTER_SECRET}
|
||||
- NEXTAUTH_EMAIL_SERVER=${NEXTAUTH_EMAIL_SERVER}
|
||||
- NEXTAUTH_EMAIL_FROM=${NEXTAUTH_EMAIL_FROM}
|
||||
ports:
|
||||
- "3000:3000"
|
||||
|
||||
# mongo:
|
||||
# extends:
|
||||
# file: databases/mongo.yml
|
||||
# service: mongo
|
||||
|
||||
# mssql:
|
||||
# extends:
|
||||
# file: databases/mssql.yml
|
||||
# service: mssql
|
||||
|
||||
# mysql:
|
||||
# extends:
|
||||
# file: databases/mysql.yml
|
||||
# service: mysql
|
||||
|
||||
# postgres:
|
||||
# extends:
|
||||
# file: databases/postgres.yml
|
||||
# service: postgres
|
||||
2521
test/docker/app/package-lock.json
generated
2521
test/docker/app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"name": "next-auth-test",
|
||||
"version": "0.0.1",
|
||||
"description": "Test application for NextAuth.js",
|
||||
"main": "",
|
||||
"scripts": {
|
||||
"dev": "next",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"author": "Iain Collins <me@iaincollins.com>",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"next": "^10.0.6",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1"
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Provider } from 'next-auth/client'
|
||||
|
||||
export default function App ({ Component, pageProps }) {
|
||||
return (
|
||||
<Provider
|
||||
options={{
|
||||
// Client Max Age controls how often the useSession in the client should
|
||||
// contact the server to sync the session state. Value in seconds.
|
||||
// e.g.
|
||||
// * 0 - Disabled (always use cache value)
|
||||
// * 60 - Sync session state with server if it's older than 60 seconds
|
||||
clientMaxAge: 0,
|
||||
// Keep Alive tells windows / tabs that are signed in to keep sending
|
||||
// a keep alive request (which extends the current session expiry) to
|
||||
// prevent sessions in open windows from expiring. Value in seconds.
|
||||
//
|
||||
// Note: If a session has expired when keep alive is triggered, all open
|
||||
// windows / tabs will be updated to reflect the user is signed out.
|
||||
keepAlive: 0
|
||||
}}
|
||||
session={pageProps.session}
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
import NextAuth from 'next-auth'
|
||||
import Providers from 'next-auth/providers'
|
||||
|
||||
// For more information on each option (and a full list of options) go to
|
||||
// https://next-auth.js.org/configuration/options
|
||||
const options = {
|
||||
// https://next-auth.js.org/configuration/providers
|
||||
providers: [
|
||||
Providers.Email({
|
||||
server: process.env.NEXTAUTH_EMAIL_SERVER,
|
||||
from: process.env.NEXTAUTH_EMAIL_FROM
|
||||
}),
|
||||
Providers.Apple({
|
||||
clientId: process.env.NEXTAUTH_APPLE_ID,
|
||||
clientSecret: {
|
||||
appleId: process.env.NEXTAUTH_APPLE_ID,
|
||||
teamId: process.env.NEXTAUTH_APPLE_TEAM_ID,
|
||||
privateKey: process.env.NEXTAUTH_APPLE_PRIVATE_KEY,
|
||||
keyId: process.env.NEXTAUTH_APPLE_KEY_ID
|
||||
}
|
||||
}),
|
||||
Providers.Auth0({
|
||||
clientId: process.env.NEXTAUTH_AUTH0_ID,
|
||||
clientSecret: process.env.NEXTAUTH_AUTH0_SECRET,
|
||||
domain: process.env.NEXTAUTH_AUTH0_DOMAIN
|
||||
}),
|
||||
Providers.Facebook({
|
||||
clientId: process.env.NEXTAUTH_FACEBOOK_ID,
|
||||
clientSecret: process.env.NEXTAUTH_FACEBOOK_SECRET
|
||||
}),
|
||||
Providers.GitHub({
|
||||
clientId: process.env.NEXTAUTH_GITHUB_ID,
|
||||
clientSecret: process.env.NEXTAUTH_GITHUB_SECRET
|
||||
}),
|
||||
Providers.Google({
|
||||
clientId: process.env.NEXTAUTH_GOOGLE_ID,
|
||||
clientSecret: process.env.NEXTAUTH_GOOGLE_SECRET
|
||||
}),
|
||||
Providers.Twitter({
|
||||
clientId: process.env.NEXTAUTH_TWITTER_ID,
|
||||
clientSecret: process.env.NEXTAUTH_TWITTER_SECRET
|
||||
})
|
||||
],
|
||||
// Database optional. MySQL, Maria DB, Postgres and MongoDB are supported.
|
||||
// https://next-auth.js.org/configuration/database
|
||||
//
|
||||
// Notes:
|
||||
// * You must to install an appropriate node_module for your database
|
||||
// * The Email provider requires a database (OAuth providers do not)
|
||||
database: process.env.NEXTAUTH_DATABASE_URL,
|
||||
|
||||
// The secret should be set to a reasonably long random string.
|
||||
// It is used to sign cookies and to sign and encrypt JSON Web Tokens, unless
|
||||
// a seperate secret is defined explicitly for encrypting the JWT.
|
||||
secret: process.env.NEXTAUTH_SECRET,
|
||||
|
||||
session: {
|
||||
// Use JSON Web Tokens for session instead of database sessions.
|
||||
// This option can be used with or without a database for users/accounts.
|
||||
// Note: `jwt` is automatically set to `true` if no database is specified.
|
||||
jwt: true
|
||||
|
||||
// Seconds - How long until an idle session expires and is no longer valid.
|
||||
// maxAge: 30 * 24 * 60 * 60, // 30 days
|
||||
|
||||
// Seconds - Throttle how frequently to write to database to extend a session.
|
||||
// Use it to limit write operations. Set to 0 to always update the database.
|
||||
// Note: This option is ignored if using JSON Web Tokens
|
||||
// updateAge: 24 * 60 * 60, // 24 hours
|
||||
},
|
||||
|
||||
// JSON Web tokens are only used for sessions if the `jwt: true` session
|
||||
// option is set - or by default if no database is specified.
|
||||
// https://next-auth.js.org/configuration/options#jwt
|
||||
jwt: {
|
||||
// A secret to use for key generation (you should set this explicitly)
|
||||
// secret: 'INp8IvdIyeMcoGAgFGoA61DdBglwwSqnXJZkgz8PSnw',
|
||||
|
||||
// Set to true to use encryption (default: false)
|
||||
// encryption: true,
|
||||
|
||||
// You can define your own encode/decode functions for signing and encryption
|
||||
// if you want to override the default behaviour.
|
||||
// async encode({ secret, token, maxAge }) {},
|
||||
// async decode({ secret, token, maxAge }) {},
|
||||
},
|
||||
|
||||
// You can define custom pages to override the built-in pages.
|
||||
// The routes shown here are the default URLs that will be used when a custom
|
||||
// pages is not specified for that route.
|
||||
// https://next-auth.js.org/configuration/pages
|
||||
pages: {
|
||||
// signIn: '/api/auth/signin', // Displays signin buttons
|
||||
// signOut: '/api/auth/signout', // Displays form with sign out button
|
||||
// error: '/api/auth/error', // Error code passed in query string as ?error=
|
||||
// verifyRequest: '/api/auth/verify-request', // Used for check email page
|
||||
// newUser: null // If set, new users will be directed here on first sign in
|
||||
},
|
||||
|
||||
// Callbacks are asynchronous functions you can use to control what happens
|
||||
// when an action is performed.
|
||||
// https://next-auth.js.org/configuration/callbacks
|
||||
callbacks: {
|
||||
// async signIn(user, account, profile) { return Promise.resolve(true) },
|
||||
// async redirect(url, baseUrl) { return Promise.resolve(baseUrl) },
|
||||
// async session(session, user) { return Promise.resolve(session) },
|
||||
// async jwt(token, user, account, profile, isNewUser) { return Promise.resolve(token) }
|
||||
},
|
||||
|
||||
// Events are useful for logging
|
||||
// https://next-auth.js.org/configuration/events
|
||||
events: { },
|
||||
|
||||
// Enable debug messages in the console if you are having problems
|
||||
debug: false
|
||||
}
|
||||
|
||||
export default (req, res) => NextAuth(req, res, options)
|
||||
@@ -1,3 +0,0 @@
|
||||
export default (req, res) => {
|
||||
res.send(JSON.stringify(process.env, null, 2))
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import jwt from 'next-auth/jwt'
|
||||
|
||||
const secret = process.env.SECRET
|
||||
|
||||
export default async (req, res) => {
|
||||
const token = await jwt.getToken({ req, secret })
|
||||
res.send(JSON.stringify(token, null, 2))
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { getSession } from 'next-auth/client'
|
||||
|
||||
export default async (req, res) => {
|
||||
const session = await getSession({ req })
|
||||
|
||||
if (session) {
|
||||
res.send({ content: 'Protected content.' })
|
||||
} else {
|
||||
res.send({ content: 'Unprotected content.' })
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { getSession } from 'next-auth/client'
|
||||
|
||||
export default async (req, res) => {
|
||||
const session = await getSession({ req })
|
||||
res.send(JSON.stringify(session, null, 2))
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import Package from 'next-auth/package.json'
|
||||
|
||||
export default (req, res) => {
|
||||
res.send(Package.version)
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export default function IndexPage () {
|
||||
return (
|
||||
<div id='nextauth-test-app'>
|
||||
<h1>NextAuth.js Test App</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { useSession } from 'next-auth/client'
|
||||
|
||||
export default function TestPage () {
|
||||
const [ session, loading ] = useSession()
|
||||
|
||||
return (
|
||||
<div id='nextauth-test-page'>
|
||||
<h1>NextAuth.js Test Page</h1>
|
||||
{session && <p id="nextauth-signed-in">Signed in</p>}
|
||||
{!session && !loading && <p id="nextauth-signed-out">Signed out</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
# Start Mongo, MSSQL, MySQL and Postgres databases on the current host running
|
||||
# on their respective default ports. This is intended for developer convenience
|
||||
# to make it easier to develop and test features manually.
|
||||
#
|
||||
# Note: Uses Docker Compose v2 as v3 doesn't currently support extends.
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
|
||||
mongo:
|
||||
extends:
|
||||
file: databases/mongo.yml
|
||||
service: mongo
|
||||
ports:
|
||||
- "27017:27017"
|
||||
|
||||
mssql:
|
||||
extends:
|
||||
file: databases/mssql.yml
|
||||
service: mssql
|
||||
ports:
|
||||
- "1433:1433"
|
||||
|
||||
mysql:
|
||||
extends:
|
||||
file: databases/mysql.yml
|
||||
service: mysql
|
||||
ports:
|
||||
- "3306:3306"
|
||||
|
||||
postgres:
|
||||
extends:
|
||||
file: databases/postgres.yml
|
||||
service: postgres
|
||||
ports:
|
||||
- "5432:5432"
|
||||
@@ -1,11 +0,0 @@
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
|
||||
mongo:
|
||||
image: bitnami/mongodb
|
||||
restart: always
|
||||
environment:
|
||||
MONGODB_USERNAME: nextauth
|
||||
MONGODB_PASSWORD: password
|
||||
MONGODB_DATABASE: nextauth
|
||||
@@ -1,13 +0,0 @@
|
||||
version: "2"
|
||||
|
||||
services:
|
||||
mssql:
|
||||
image: mcr.microsoft.com/mssql/server:2017-latest
|
||||
restart: always
|
||||
environment:
|
||||
SA_PASSWORD: Pa55w0rd # minimum password complexity
|
||||
ACCEPT_EULA: Y
|
||||
# WARN: command overrides, default image start sequence, start.sh starts 'sql-server'
|
||||
command: '/var/setup/start.sh'
|
||||
volumes:
|
||||
- ./mssql:/var/setup # mount setup files
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
# see https://github.com/Microsoft/mssql-docker
|
||||
# no way to know when sql server is ready
|
||||
until /opt/mssql-tools/bin/sqlcmd -S 127.0.01 -U sa -P Pa55w0rd -d master -i /var/setup/setup.sql
|
||||
do sleep 1;
|
||||
done
|
||||
echo "NEXT_AUTH: setup completed"
|
||||
@@ -1,29 +0,0 @@
|
||||
USE master;
|
||||
/* did you tear down the container ? */
|
||||
if not exists (select name
|
||||
from sys.syslogins
|
||||
where name = 'nextauth')
|
||||
CREATE LOGIN nextauth
|
||||
WITH PASSWORD = 'password',
|
||||
CHECK_POLICY = OFF;
|
||||
GO
|
||||
/* did you tear down the container ? */
|
||||
if not exists (select name
|
||||
from sys.databases
|
||||
where name = 'nextauth' )
|
||||
CREATE database nextauth
|
||||
GO
|
||||
/* did you tear down the container ? */
|
||||
if not exists(select [name]
|
||||
from sys.sysusers
|
||||
where name= 'nextauth')
|
||||
CREATE USER nextauth
|
||||
WITH DEFAULT_SCHEMA =[dbo];
|
||||
GO
|
||||
/*
|
||||
* Adding user as sysadmin,
|
||||
* So you can easily drop/create/re-create/alter the database
|
||||
* You will need to login to 'master' to do that
|
||||
*/
|
||||
exec sp_addsrvrolemember @loginame = N'nextauth', @rolename = N'sysadmin'
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
# launch setup on the background & start server
|
||||
# otherise sqlservr won't start
|
||||
/var/setup/setup.sh & /opt/mssql/bin/sqlservr
|
||||
@@ -1,13 +0,0 @@
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
|
||||
mysql:
|
||||
image: mysql
|
||||
command: --default-authentication-plugin=mysql_native_password
|
||||
restart: always
|
||||
environment:
|
||||
MYSQL_USER: nextauth
|
||||
MYSQL_PASSWORD: password
|
||||
MYSQL_DATABASE: nextauth
|
||||
MYSQL_RANDOM_ROOT_PASSWORD: 'yes'
|
||||
@@ -1,11 +0,0 @@
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
|
||||
postgres:
|
||||
image: postgres
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_USER: nextauth
|
||||
POSTGRES_PASSWORD: password
|
||||
POSTGRES_DB: nextauth
|
||||
141
test/fixtures/schemas/mssql.json
vendored
141
test/fixtures/schemas/mssql.json
vendored
@@ -1,141 +0,0 @@
|
||||
{
|
||||
"users": {
|
||||
"id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"name": {
|
||||
"type": "varchar",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"email": {
|
||||
"type": "varchar",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"email_verified": {
|
||||
"type": "datetime",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"image": {
|
||||
"type": "varchar",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"created_at": {
|
||||
"type": "datetime",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "datetime",
|
||||
"nullable": false
|
||||
}
|
||||
},
|
||||
"accounts": {
|
||||
"id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"compound_id": {
|
||||
"type": "varchar",
|
||||
"nullable": false
|
||||
},
|
||||
"user_id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_type": {
|
||||
"type": "varchar",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_id": {
|
||||
"type": "varchar",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_account_id": {
|
||||
"type": "varchar",
|
||||
"nullable": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"type": "text",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"access_token": {
|
||||
"type": "text",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"access_token_expires": {
|
||||
"type": "datetime",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"created_at": {
|
||||
"type": "datetime",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "datetime",
|
||||
"nullable": false
|
||||
}
|
||||
},
|
||||
"sessions": {
|
||||
"id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"user_id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"expires": {
|
||||
"type": "datetime",
|
||||
"nullable": false
|
||||
},
|
||||
"session_token": {
|
||||
"type": "varchar",
|
||||
"nullable": false
|
||||
},
|
||||
"access_token": {
|
||||
"type": "varchar",
|
||||
"nullable": false
|
||||
},
|
||||
"created_at": {
|
||||
"type": "datetime",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "datetime",
|
||||
"nullable": false
|
||||
}
|
||||
},
|
||||
"verification_requests": {
|
||||
"id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"identifier": {
|
||||
"type": "varchar",
|
||||
"nullable": false
|
||||
},
|
||||
"token": {
|
||||
"type": "varchar",
|
||||
"nullable": false
|
||||
},
|
||||
"expires": {
|
||||
"type": "datetime",
|
||||
"nullable": false
|
||||
},
|
||||
"created_at": {
|
||||
"type": "datetime",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "datetime",
|
||||
"nullable": false
|
||||
}
|
||||
}
|
||||
}
|
||||
141
test/fixtures/schemas/mysql.json
vendored
141
test/fixtures/schemas/mysql.json
vendored
@@ -1,141 +0,0 @@
|
||||
{
|
||||
"users": {
|
||||
"id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"name": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"email": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"email_verified": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"image": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"created_at": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
}
|
||||
},
|
||||
"accounts": {
|
||||
"id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"compound_id": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": false
|
||||
},
|
||||
"user_id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_type": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_id": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_account_id": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"type": "text",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"access_token": {
|
||||
"type": "text",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"access_token_expires": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"created_at": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
}
|
||||
},
|
||||
"sessions": {
|
||||
"id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"user_id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"expires": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
},
|
||||
"session_token": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": false
|
||||
},
|
||||
"access_token": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": false
|
||||
},
|
||||
"created_at": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
}
|
||||
},
|
||||
"verification_requests": {
|
||||
"id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"identifier": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": false
|
||||
},
|
||||
"token": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": false
|
||||
},
|
||||
"expires": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
},
|
||||
"created_at": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
}
|
||||
}
|
||||
}
|
||||
141
test/fixtures/schemas/postgres.json
vendored
141
test/fixtures/schemas/postgres.json
vendored
@@ -1,141 +0,0 @@
|
||||
{
|
||||
"users": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"nullable": false
|
||||
},
|
||||
"name": {
|
||||
"type": "character varying",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"email": {
|
||||
"type": "character varying",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"email_verified": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"image": {
|
||||
"type": "character varying",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"created_at": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
}
|
||||
},
|
||||
"accounts": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"nullable": false
|
||||
},
|
||||
"compound_id": {
|
||||
"type": "character varying",
|
||||
"nullable": false
|
||||
},
|
||||
"user_id": {
|
||||
"type": "integer",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_type": {
|
||||
"type": "character varying",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_id": {
|
||||
"type": "character varying",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_account_id": {
|
||||
"type": "character varying",
|
||||
"nullable": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"type": "text",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"access_token": {
|
||||
"type": "text",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"access_token_expires": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"created_at": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
}
|
||||
},
|
||||
"sessions": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"nullable": false
|
||||
},
|
||||
"user_id": {
|
||||
"type": "integer",
|
||||
"nullable": false
|
||||
},
|
||||
"expires": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
},
|
||||
"session_token": {
|
||||
"type": "character varying",
|
||||
"nullable": false
|
||||
},
|
||||
"access_token": {
|
||||
"type": "character varying",
|
||||
"nullable": false
|
||||
},
|
||||
"created_at": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
}
|
||||
},
|
||||
"verification_requests": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"nullable": false
|
||||
},
|
||||
"identifier": {
|
||||
"type": "character varying",
|
||||
"nullable": false
|
||||
},
|
||||
"token": {
|
||||
"type": "character varying",
|
||||
"nullable": false
|
||||
},
|
||||
"expires": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
},
|
||||
"created_at": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
}
|
||||
}
|
||||
}
|
||||
47
test/fixtures/sql/mssql.sql
vendored
47
test/fixtures/sql/mssql.sql
vendored
@@ -1,47 +0,0 @@
|
||||
-- FIXME Missing indexes!
|
||||
CREATE TABLE accounts
|
||||
(
|
||||
id int IDENTITY(1,1) NOT NULL,
|
||||
compound_id varchar(255) NOT NULL,
|
||||
user_id int NOT NULL,
|
||||
provider_type varchar(255) NOT NULL,
|
||||
provider_id varchar(255) NOT NULL,
|
||||
provider_account_id varchar(255) NOT NULL,
|
||||
refresh_token text NULL,
|
||||
access_token text NULL,
|
||||
access_token_expires datetime NULL,
|
||||
created_at datetime NOT NULL DEFAULT getdate(),
|
||||
updated_at datetime NOT NULL DEFAULT getdate()
|
||||
);
|
||||
|
||||
CREATE TABLE sessions
|
||||
(
|
||||
id int IDENTITY(1,1) NOT NULL,
|
||||
user_id int NOT NULL,
|
||||
expires datetime NOT NULL,
|
||||
session_token varchar(255) NOT NULL,
|
||||
access_token varchar(255) NOT NULL,
|
||||
created_at datetime NOT NULL DEFAULT getdate(),
|
||||
updated_at datetime NOT NULL DEFAULT getdate()
|
||||
);
|
||||
|
||||
CREATE TABLE users
|
||||
(
|
||||
id int IDENTITY(1,1) NOT NULL,
|
||||
name varchar(255) NULL,
|
||||
email varchar(255) NULL,
|
||||
email_verified datetime NULL,
|
||||
image varchar(255) NULL,
|
||||
created_at datetime NOT NULL DEFAULT getdate(),
|
||||
updated_at datetime NOT NULL DEFAULT getdate()
|
||||
);
|
||||
|
||||
CREATE TABLE verification_requests
|
||||
(
|
||||
id int IDENTITY(1,1) NOT NULL,
|
||||
identifier varchar(255) NOT NULL,
|
||||
token varchar(255) NOT NULL,
|
||||
expires datetime NOT NULL,
|
||||
created_at datetime NOT NULL DEFAULT getdate(),
|
||||
updated_at datetime NOT NULL DEFAULT getdate()
|
||||
);
|
||||
74
test/fixtures/sql/mysql.sql
vendored
74
test/fixtures/sql/mysql.sql
vendored
@@ -1,74 +0,0 @@
|
||||
CREATE TABLE accounts
|
||||
(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
compound_id VARCHAR(255) NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
provider_type VARCHAR(255) NOT NULL,
|
||||
provider_id VARCHAR(255) NOT NULL,
|
||||
provider_account_id VARCHAR(255) NOT NULL,
|
||||
refresh_token TEXT,
|
||||
access_token TEXT,
|
||||
access_token_expires TIMESTAMP(6),
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE sessions
|
||||
(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
expires TIMESTAMP(6) NOT NULL,
|
||||
session_token VARCHAR(255) NOT NULL,
|
||||
access_token VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE users
|
||||
(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
name VARCHAR(255),
|
||||
email VARCHAR(255),
|
||||
email_verified TIMESTAMP(6),
|
||||
image VARCHAR(255),
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE verification_requests
|
||||
(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
identifier VARCHAR(255) NOT NULL,
|
||||
token VARCHAR(255) NOT NULL,
|
||||
expires TIMESTAMP(6) NOT NULL,
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX compound_id
|
||||
ON accounts(compound_id);
|
||||
|
||||
CREATE INDEX provider_account_id
|
||||
ON accounts(provider_account_id);
|
||||
|
||||
CREATE INDEX provider_id
|
||||
ON accounts(provider_id);
|
||||
|
||||
CREATE INDEX user_id
|
||||
ON accounts(user_id);
|
||||
|
||||
CREATE UNIQUE INDEX session_token
|
||||
ON sessions(session_token);
|
||||
|
||||
CREATE UNIQUE INDEX access_token
|
||||
ON sessions(access_token);
|
||||
|
||||
CREATE UNIQUE INDEX email
|
||||
ON users(email);
|
||||
|
||||
CREATE UNIQUE INDEX token
|
||||
ON verification_requests(token);
|
||||
74
test/fixtures/sql/postgres.sql
vendored
74
test/fixtures/sql/postgres.sql
vendored
@@ -1,74 +0,0 @@
|
||||
CREATE TABLE accounts
|
||||
(
|
||||
id SERIAL,
|
||||
compound_id VARCHAR(255) NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
provider_type VARCHAR(255) NOT NULL,
|
||||
provider_id VARCHAR(255) NOT NULL,
|
||||
provider_account_id VARCHAR(255) NOT NULL,
|
||||
refresh_token TEXT,
|
||||
access_token TEXT,
|
||||
access_token_expires TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE sessions
|
||||
(
|
||||
id SERIAL,
|
||||
user_id INTEGER NOT NULL,
|
||||
expires TIMESTAMPTZ NOT NULL,
|
||||
session_token VARCHAR(255) NOT NULL,
|
||||
access_token VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE users
|
||||
(
|
||||
id SERIAL,
|
||||
name VARCHAR(255),
|
||||
email VARCHAR(255),
|
||||
email_verified TIMESTAMPTZ,
|
||||
image VARCHAR(255),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE verification_requests
|
||||
(
|
||||
id SERIAL,
|
||||
identifier VARCHAR(255) NOT NULL,
|
||||
token VARCHAR(255) NOT NULL,
|
||||
expires TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX compound_id
|
||||
ON accounts(compound_id);
|
||||
|
||||
CREATE INDEX provider_account_id
|
||||
ON accounts(provider_account_id);
|
||||
|
||||
CREATE INDEX provider_id
|
||||
ON accounts(provider_id);
|
||||
|
||||
CREATE INDEX user_id
|
||||
ON accounts(user_id);
|
||||
|
||||
CREATE UNIQUE INDEX session_token
|
||||
ON sessions(session_token);
|
||||
|
||||
CREATE UNIQUE INDEX access_token
|
||||
ON sessions(access_token);
|
||||
|
||||
CREATE UNIQUE INDEX email
|
||||
ON users(email);
|
||||
|
||||
CREATE UNIQUE INDEX token
|
||||
ON verification_requests(token);
|
||||
@@ -1,71 +0,0 @@
|
||||
require('dotenv').config()
|
||||
const assert = require('assert')
|
||||
const { puppeteer, puppeteerOptions } = require('../lib/puppeteer')
|
||||
|
||||
const BASE_URL = 'http://localhost:3000'
|
||||
const CALLBACK_URL = `${BASE_URL}/test`
|
||||
|
||||
const {
|
||||
NEXTAUTH_GITHUB_USERNAME: USERNAME,
|
||||
NEXTAUTH_GITHUB_PASSWORD: PASSWORD
|
||||
} = process.env
|
||||
|
||||
describe('GitHub (OAuth 2.0 flow)', function () {
|
||||
this.slow(5000)
|
||||
this.timeout(1000 * 60)
|
||||
let browser,page
|
||||
|
||||
before(async () => {
|
||||
browser = await puppeteer.launch(puppeteerOptions)
|
||||
page = await browser.newPage()
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should show button on sign in page', async function () {
|
||||
page.setDefaultTimeout(1000 * 60)
|
||||
await page.goto(`${BASE_URL}/api/auth/signin?callbackUrl=${encodeURIComponent(CALLBACK_URL)}`)
|
||||
await page.waitForSelector(`form[action="${BASE_URL}/api/auth/signin/github"] button`)
|
||||
await page.click(`form[action="${BASE_URL}/api/auth/signin/github"] button`)
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be redirected to provider', async function () {
|
||||
// Enter username
|
||||
await page.waitForSelector('input[name="login"]')
|
||||
await page.click('input[name="login"]')
|
||||
await page.keyboard.type(USERNAME)
|
||||
|
||||
// Enter password
|
||||
await page.waitForSelector('input[name="password"]')
|
||||
await page.click('input[name="password"]')
|
||||
await page.keyboard.type(PASSWORD)
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be able to sign in with provider', async function () {
|
||||
// Click submit
|
||||
await page.click('form[action="/session"] [type="submit"]')
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be returned to callback URL', async function () {
|
||||
// Wait for page to return to callback URL
|
||||
await page.waitForSelector('#nextauth-test-page')
|
||||
|
||||
// Check we are at the correct callback URL
|
||||
assert.equal(page.url(), CALLBACK_URL)
|
||||
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be signed in', async function () {
|
||||
// Check we are signed in
|
||||
await page.waitForSelector('#nextauth-signed-in')
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await browser.close()
|
||||
return Promise.resolve()
|
||||
})
|
||||
})
|
||||
@@ -1,81 +0,0 @@
|
||||
require('dotenv').config()
|
||||
const assert = require('assert')
|
||||
const { puppeteer, puppeteerOptions } = require('../lib/puppeteer')
|
||||
|
||||
const BASE_URL = 'http://localhost:3000'
|
||||
const CALLBACK_URL = `${BASE_URL}/test`
|
||||
|
||||
const {
|
||||
NEXTAUTH_GOOGLE_USERNAME: USERNAME,
|
||||
NEXTAUTH_GOOGLE_PASSWORD: PASSWORD
|
||||
} = process.env
|
||||
|
||||
// This seems to stall because of a popup that is displayed only when using
|
||||
// puppeteer. See FIXME below. Would appreciate any help resolving it.
|
||||
describe.skip('Google (OAuth 2.0 flow)', function () {
|
||||
this.slow(5000)
|
||||
this.timeout(1000 * 60)
|
||||
let browser,page
|
||||
|
||||
before(async () => {
|
||||
browser = await puppeteer.launch(puppeteerOptions)
|
||||
page = await browser.newPage()
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should show button on sign in page', async function () {
|
||||
page.setDefaultTimeout(1000 * 60)
|
||||
await page.goto(`${BASE_URL}/api/auth/signin?callbackUrl=${encodeURIComponent(CALLBACK_URL)}`)
|
||||
await page.waitForSelector(`form[action="${BASE_URL}/api/auth/signin/google"] button`)
|
||||
await page.click(`form[action="${BASE_URL}/api/auth/signin/google"] button`)
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be redirected to provider', async function () {
|
||||
// Enter username
|
||||
await page.waitForSelector('input[type="email"]')
|
||||
await page.click('input[type="email"]')
|
||||
await page.keyboard.type(USERNAME)
|
||||
|
||||
// FIXME Work out how to dismiss popup
|
||||
// A popup *only* appears when using puppeteer (not manually) but I can't
|
||||
// get the xPath selectors to work to dismiss it. This is as close as I got.
|
||||
// await page.waitForXPath("(//span[contains(text(), 'Got it')])[2]")
|
||||
// const element = await page.$x("(//span[contains(text(), 'Got it')])[2]")
|
||||
// await element.click()
|
||||
|
||||
// Enter password
|
||||
await page.waitForSelector('input[type="password"]')
|
||||
await page.click('input[type="password"]')
|
||||
await page.keyboard.type(PASSWORD)
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be able to sign in with provider', async function () {
|
||||
// Click submit
|
||||
await page.click('button[type="button"]')
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be returned to callback URL', async function () {
|
||||
// Wait for page to return to callback URL
|
||||
await page.waitForSelector('#nextauth-test-page')
|
||||
|
||||
// Check we are at the correct callback URL
|
||||
// Note: Google OAuth appends a # to the end of the URL in Chrome
|
||||
assert.equal(page.url(), `${CALLBACK_URL}#`)
|
||||
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be signed in', async function () {
|
||||
// Check we are signed in
|
||||
await page.waitForSelector('#nextauth-signed-in')
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await browser.close()
|
||||
return Promise.resolve()
|
||||
})
|
||||
})
|
||||
@@ -1,71 +0,0 @@
|
||||
require('dotenv').config()
|
||||
const assert = require('assert')
|
||||
const { puppeteer, puppeteerOptions } = require('../lib/puppeteer')
|
||||
|
||||
const BASE_URL = 'http://localhost:3000'
|
||||
const CALLBACK_URL = `${BASE_URL}/test`
|
||||
|
||||
const {
|
||||
NEXTAUTH_TWITTER_USERNAME: USERNAME,
|
||||
NEXTAUTH_TWITTER_PASSWORD: PASSWORD,
|
||||
} = process.env
|
||||
|
||||
describe('Twitter (OAuth 1.1 flow)', async function () {
|
||||
this.slow(5000)
|
||||
this.timeout(1000 * 60)
|
||||
let browser,page
|
||||
|
||||
before(async () => {
|
||||
browser = await puppeteer.launch(puppeteerOptions)
|
||||
page = await browser.newPage()
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should show button on sign in page', async function () {
|
||||
page.setDefaultTimeout(1000 * 60)
|
||||
await page.goto(`${BASE_URL}/api/auth/signin?callbackUrl=${encodeURIComponent(CALLBACK_URL)}`)
|
||||
await page.waitForSelector(`form[action="${BASE_URL}/api/auth/signin/twitter"] button`)
|
||||
await page.click(`form[action="${BASE_URL}/api/auth/signin/twitter"] button`)
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be redirected to provider', async function () {
|
||||
// Enter username
|
||||
await page.waitForSelector('input[name="session[username_or_email]"]')
|
||||
await page.click('input[name="session[username_or_email]"]')
|
||||
await page.keyboard.type(USERNAME)
|
||||
|
||||
// Enter password
|
||||
await page.waitForSelector('input[name="session[password]"]')
|
||||
await page.click('input[name="session[password]"]')
|
||||
await page.keyboard.type(PASSWORD)
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be able to sign in with provider', async function () {
|
||||
// Click submit
|
||||
await page.click('form[action="https://api.twitter.com/oauth/authenticate"] [type="submit"]')
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be returned to callback URL', async function () {
|
||||
// Wait for page to return to callback URL
|
||||
await page.waitForSelector('#nextauth-test-page')
|
||||
|
||||
// Check we are at the correct callback URL
|
||||
assert.equal(page.url(), CALLBACK_URL)
|
||||
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be signed in', async function () {
|
||||
// Check we are signed in
|
||||
await page.waitForSelector('#nextauth-signed-in')
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await browser.close()
|
||||
return Promise.resolve()
|
||||
})
|
||||
})
|
||||
@@ -1,36 +0,0 @@
|
||||
exports.compareSchemas = (expected, actual) => {
|
||||
const errors = []
|
||||
|
||||
// Check all models and properties that are expected exist
|
||||
for (const objectName in expected) {
|
||||
if (actual[objectName]) {
|
||||
for (const propertyName in expected[objectName]) {
|
||||
if (actual[objectName][propertyName]) {
|
||||
if (JSON.stringify(expected[objectName][propertyName]) !== JSON.stringify(actual[objectName][propertyName])) {
|
||||
errors.push(`${objectName}.${propertyName} does not match expected result`)
|
||||
}
|
||||
} else {
|
||||
errors.push(`${objectName}.${propertyName} not found (should exist)`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
errors.push(`${objectName} not found (should exist)`)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for models and properties that exist but are not expected
|
||||
for (const objectName in actual) {
|
||||
if (expected[objectName]) {
|
||||
for (const propertyName in actual[objectName]) {
|
||||
if (!expected[objectName][propertyName]) {
|
||||
errors.push(`${objectName}.${propertyName} found (not expected)`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
errors.push(`${objectName} found (not expected)`)
|
||||
}
|
||||
}
|
||||
|
||||
// Return true if no errors, else return array of errors
|
||||
return errors.length > 0 ? errors : true
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
const puppeteerExtra = require('puppeteer-extra')
|
||||
const StealthPlugin = require("puppeteer-extra-plugin-stealth")
|
||||
const stealthPlugin = StealthPlugin()
|
||||
|
||||
// Override Puppeteer user agent to set 'navigator.platform' explicitly to
|
||||
// prevent detection on some providers (e.g. GitHub OAuth) as they force 2FA
|
||||
// on sign in if they detect sign in from a platform they haven't seen before.
|
||||
const puppeteerExtraPluginUserAgentOverride = require("puppeteer-extra-plugin-stealth/evasions/user-agent-override")
|
||||
stealthPlugin.enabledEvasions.delete("user-agent-override")
|
||||
puppeteerExtra.use(stealthPlugin)
|
||||
const pluginUserAgentOverride = puppeteerExtraPluginUserAgentOverride({
|
||||
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36",
|
||||
platform: "MacIntel"
|
||||
})
|
||||
puppeteerExtra.use(pluginUserAgentOverride)
|
||||
|
||||
// CI is set to true by GitHub Actions to indicate is running in CD/CI
|
||||
const { CI } = process.env
|
||||
|
||||
const puppeteerOptions = {
|
||||
headless: true // Set to 'false' to debug more easily
|
||||
}
|
||||
|
||||
// When running on remote test runner (which is ARM) the executable path
|
||||
// needs to be set to 'chromium-browser' so it uses the ARM build of Chromium
|
||||
// not the x86 build that Puppeteer uses by default. Supporting this allows us
|
||||
// to test easily from remote locations that are outside cloud networks like
|
||||
// AWS, GPC, Azure, etc. and avoids tests being thwarted by IP blocklists.
|
||||
if (CI)
|
||||
puppeteerOptions.executablePath = 'chromium-browser'
|
||||
|
||||
module.exports = {
|
||||
puppeteer: puppeteerExtra,
|
||||
puppeteerOptions
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/* eslint-disable */
|
||||
// Placeholder for schema test (will use test framework, this is temporary)
|
||||
const Adapters = require('../adapters')
|
||||
|
||||
;(async () => {
|
||||
try {
|
||||
// We can't connection a local MongoDB SRV instance but we can at least see if the URLs cause an error
|
||||
Adapters.Default('mongodb+srv://nextauth:password@127.0.0.1/nextauth?ssl=false&retryWrites=true')
|
||||
|
||||
// Connect to local MongoDB instance
|
||||
// Note: MongoDB doesn't thrown a connection error right away if is a
|
||||
// problem with the credentials or host configuration, but after a few
|
||||
// seconds it throws a Timeout error (which is caught by the adapter).
|
||||
const adapter = Adapters.Default('mongodb://nextauth:password@127.0.0.1:27017/nextauth?synchronize=true')
|
||||
await adapter.getAdapter()
|
||||
|
||||
// @TODO create objects in database, check format of objects returned
|
||||
|
||||
console.log('MongoDB loaded ok')
|
||||
process.exit()
|
||||
} catch (error) {
|
||||
console.error('MongoDB error', error)
|
||||
process.exit(1)
|
||||
}
|
||||
})()
|
||||
106
test/mssql.js
106
test/mssql.js
@@ -1,106 +0,0 @@
|
||||
/* eslint-disable */
|
||||
// Placeholder for schema test (will use test framework, this is temporary)
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const mssql = require('mssql');
|
||||
const Adapters = require('../adapters');
|
||||
|
||||
const SCHEMA_FILE = path.join(__dirname, '/fixtures/schemas/mssql.json');
|
||||
const expectedSchema = JSON.parse(fs.readFileSync(SCHEMA_FILE));
|
||||
const TABLES = Object.keys(expectedSchema);
|
||||
const databaseUrl = `mssql://nextauth:password@127.0.0.1:1433/nextauth?synchronize=true`;
|
||||
|
||||
function printSchema() {
|
||||
return new Promise(async (resolve) => {
|
||||
/**
|
||||
* @type {import('mssql').ConnectionPool}
|
||||
*/
|
||||
let connection;
|
||||
try {
|
||||
connection = await mssql.connect(databaseUrl);
|
||||
// Invoke adapter to sync schema
|
||||
await (Adapters.Default(databaseUrl)).getAdapter();
|
||||
// query schema
|
||||
const { recordset } = await connection.query(
|
||||
`use [nextauth]; ` +
|
||||
TABLES.map(
|
||||
(table) =>
|
||||
`select * from INFORMATION_SCHEMA.COLUMNS` +
|
||||
` where TABLE_NAME = '${table}'`
|
||||
).join(' UNION ALL ')
|
||||
);
|
||||
// build result
|
||||
return resolve(
|
||||
TABLES.reduce(
|
||||
(out, next) => ({
|
||||
...out,
|
||||
[next]: collect(recordset, next),
|
||||
}),
|
||||
{}
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
} finally {
|
||||
if (connection) {
|
||||
connection.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
const assert = require('assert');
|
||||
/** RUN */
|
||||
(async () => {
|
||||
try {
|
||||
const testResultSchema = await printSchema();
|
||||
const actualTables = Object.keys(testResultSchema);
|
||||
assert.equal(
|
||||
TABLES,
|
||||
actualTables.join(),
|
||||
`MSSQL Schema: Expected tables [${TABLES.join()}]\n to be [${actualTables.join()}]`
|
||||
);
|
||||
//cheap deepEquals, with hints
|
||||
for (const tableName of TABLES) {
|
||||
const newLocal = expectedSchema[tableName];
|
||||
for (const columnName of Object.keys(newLocal)) {
|
||||
const expected = expectedSchema[tableName][columnName];
|
||||
const actual = testResultSchema[tableName][columnName];
|
||||
for (const propKey of Object.keys(expected)) {
|
||||
assert.equal(
|
||||
expected[propKey],
|
||||
actual[propKey],
|
||||
`Expected ${tableName}.${columnName}.${propKey}=${actual[propKey]}` +
|
||||
` to be ${expected[propKey]}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('mssql: schema ok');
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
})()
|
||||
.then(() => process.exit())
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(-1);
|
||||
});
|
||||
/** collect results */
|
||||
const collect = (records, tableName) => {
|
||||
const keys = Object.keys(expectedSchema[tableName]);
|
||||
const ret = records
|
||||
.filter((x) => x.TABLE_NAME === tableName)
|
||||
.reduce((out, x) => {
|
||||
if (keys.indexOf(x.COLUMN_NAME) === -1) return out; //map only required columns
|
||||
const nullable = x.IS_NULLABLE === 'YES';
|
||||
return {
|
||||
...out,
|
||||
[x.COLUMN_NAME]: {
|
||||
nullable,
|
||||
type: x.DATA_TYPE,
|
||||
default: (nullable && x.COLUMN_DEFAULT) || undefined,
|
||||
},
|
||||
};
|
||||
}, {});
|
||||
return ret;
|
||||
};
|
||||
@@ -1,74 +0,0 @@
|
||||
/* eslint-disable */
|
||||
// Placeholder for schema test (will use test framework, this is temporary)
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const mysql = require('mysql')
|
||||
|
||||
const { compareSchemas } = require('./lib/db')
|
||||
const Adapters = require('../adapters')
|
||||
|
||||
const TABLES = ['users', 'accounts', 'sessions', 'verification_requests']
|
||||
const SCHEMA_FILE = path.join(__dirname, '/fixtures/schemas/mysql.json')
|
||||
|
||||
function printSchema () {
|
||||
return new Promise(async (resolve) => {
|
||||
// Invoke adapter to sync schema
|
||||
const adapter = Adapters.Default('mysql://nextauth:password@127.0.0.1:3306/nextauth?synchronize=true')
|
||||
await adapter.getAdapter()
|
||||
|
||||
const connection = mysql.createConnection({
|
||||
host: '127.0.0.1',
|
||||
user: 'nextauth',
|
||||
password: 'password',
|
||||
database: 'nextauth',
|
||||
port: 3306,
|
||||
multipleStatements: true
|
||||
})
|
||||
|
||||
connection.connect()
|
||||
connection.query(
|
||||
TABLES.map(table => `DESCRIBE ${table}`).join(';'),
|
||||
(error, result) => {
|
||||
if (error) { throw error }
|
||||
|
||||
const getColumnSchema = (column) => {
|
||||
const nullable = column.Null === 'YES' ? true : false
|
||||
return {
|
||||
type: column.Type,
|
||||
nullable,
|
||||
default: nullable ? column.Default : undefined
|
||||
}
|
||||
}
|
||||
|
||||
const users = {}
|
||||
const accounts = {}
|
||||
const sessions = {}
|
||||
const verification_requests = {}
|
||||
|
||||
result[0].forEach(column => { users[column.Field] = getColumnSchema(column) })
|
||||
result[1].forEach(column => { accounts[column.Field] = getColumnSchema(column) })
|
||||
result[2].forEach(column => { sessions[column.Field] = getColumnSchema(column) })
|
||||
result[3].forEach(column => { verification_requests[column.Field] = getColumnSchema(column) })
|
||||
|
||||
connection.end()
|
||||
|
||||
resolve({ users, accounts, sessions, verification_requests })
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const expectedSchema = JSON.parse(fs.readFileSync(SCHEMA_FILE))
|
||||
const testResultSchema = await printSchema()
|
||||
const compareResult = compareSchemas(expectedSchema, testResultSchema)
|
||||
if (compareResult === true) {
|
||||
console.log('MySQL schema ok')
|
||||
process.exit()
|
||||
} else {
|
||||
console.error('MySQL schema errors')
|
||||
compareResult.forEach(error => console.log(` * ${error}`))
|
||||
console.log('MySQL schema found:', JSON.stringify(testResultSchema, null, 2))
|
||||
process.exit(1)
|
||||
}
|
||||
})()
|
||||
@@ -1,73 +0,0 @@
|
||||
/* eslint-disable */
|
||||
// Placeholder for schema test (will use test framework, this is temporary)
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { Client } = require('pg')
|
||||
|
||||
const { compareSchemas } = require('./lib/db')
|
||||
const Adapters = require('../adapters')
|
||||
|
||||
const TABLES = ['users', 'accounts', 'sessions', 'verification_requests']
|
||||
const SCHEMA_FILE = path.join(__dirname, '/fixtures/schemas/postgres.json')
|
||||
|
||||
function printSchema () {
|
||||
return new Promise(async (resolve) => {
|
||||
// Invoke adapter to sync schema
|
||||
const adapter = Adapters.Default('postgres://nextauth:password@127.0.0.1:5432/nextauth?synchronize=true')
|
||||
await adapter.getAdapter()
|
||||
|
||||
const connection = new Client({
|
||||
host: '127.0.0.1',
|
||||
user: 'nextauth',
|
||||
password: 'password',
|
||||
database: 'nextauth',
|
||||
port: 5432
|
||||
})
|
||||
|
||||
connection.connect()
|
||||
connection.query(
|
||||
TABLES.map(table => `SELECT column_name, data_type, character_maximum_length, is_nullable, column_default FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = '${table}' ORDER BY ordinal_position`).join(';'),
|
||||
(error, result) => {
|
||||
if (error) { throw error }
|
||||
|
||||
const getColumnSchema = (column) => {
|
||||
const nullable = column.is_nullable === 'YES' ? true : false
|
||||
return {
|
||||
type: column.data_type,
|
||||
nullable,
|
||||
default: nullable ? column.column_default : undefined
|
||||
}
|
||||
}
|
||||
|
||||
const users = {}
|
||||
const accounts = {}
|
||||
const sessions = {}
|
||||
const verification_requests = {}
|
||||
|
||||
result[0].rows.forEach(column => { users[column.column_name] = getColumnSchema(column) })
|
||||
result[1].rows.forEach(column => { accounts[column.column_name] = getColumnSchema(column) })
|
||||
result[2].rows.forEach(column => { sessions[column.column_name] = getColumnSchema(column) })
|
||||
result[3].rows.forEach(column => { verification_requests[column.column_name] = getColumnSchema(column) })
|
||||
|
||||
connection.end()
|
||||
|
||||
resolve({ users, accounts, sessions, verification_requests })
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const expectedSchema = JSON.parse(fs.readFileSync(SCHEMA_FILE))
|
||||
const testResultSchema = await printSchema()
|
||||
const compareResult = compareSchemas(expectedSchema, testResultSchema)
|
||||
if (compareResult === true) {
|
||||
console.log('Postgres schema ok')
|
||||
process.exit()
|
||||
} else {
|
||||
console.error('Postgres schema errors')
|
||||
compareResult.forEach(error => console.log(` * ${error}`))
|
||||
console.log('Postgres schema found:', JSON.stringify(testResultSchema, null, 2))
|
||||
process.exit(1)
|
||||
}
|
||||
})()
|
||||
348
types/adapters.d.ts
vendored
348
types/adapters.d.ts
vendored
@@ -1,245 +1,131 @@
|
||||
import { AppOptions } from "./internals"
|
||||
import { ConnectionOptions, EntitySchema } from "typeorm"
|
||||
import { User } from "."
|
||||
import { AppProvider } from "./providers"
|
||||
import { User, Profile, Session } from "."
|
||||
import { EmailConfig, SendVerificationRequest } from "./providers"
|
||||
import { ConnectionOptions } from "typeorm"
|
||||
|
||||
export interface Profile {
|
||||
id: string
|
||||
name: string
|
||||
email: string | null
|
||||
image?: string | null
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
userId: string | number | object
|
||||
expires: Date
|
||||
sessionToken: string
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
export interface VerificationRequest {
|
||||
identifier: string
|
||||
token: string
|
||||
expires: Date
|
||||
}
|
||||
|
||||
export interface SendVerificationRequestParams {
|
||||
identifier: string
|
||||
url: string
|
||||
token: string
|
||||
baseUrl: string
|
||||
provider: AppProvider
|
||||
}
|
||||
|
||||
export type EmailAppProvider = AppProvider & {
|
||||
sendVerificationRequest: (
|
||||
params: SendVerificationRequestParams
|
||||
) => Promise<void>
|
||||
maxAge: number | undefined
|
||||
}
|
||||
|
||||
export interface AdapterInstance<
|
||||
TUser,
|
||||
TProfile,
|
||||
TSession,
|
||||
TVerificationRequest
|
||||
> {
|
||||
createUser: (profile: TProfile) => Promise<TUser>
|
||||
getUser: (id: string) => Promise<TUser | null>
|
||||
getUserByEmail: (email: string) => Promise<TUser | null>
|
||||
getUserByProviderAccountId: (
|
||||
providerId: string,
|
||||
providerAccountId: string
|
||||
) => Promise<TUser | null>
|
||||
updateUser: (user: TUser) => Promise<TUser>
|
||||
linkAccount: (
|
||||
userId: string,
|
||||
providerId: string,
|
||||
providerType: string,
|
||||
providerAccountId: string,
|
||||
refreshToken: string,
|
||||
accessToken: string,
|
||||
accessTokenExpires: number
|
||||
) => Promise<void>
|
||||
createSession: (user: TUser) => Promise<TSession>
|
||||
getSession: (sessionToken: string) => Promise<TSession | null>
|
||||
updateSession: (session: TSession, force?: boolean) => Promise<TSession>
|
||||
deleteSession: (sessionToken: string) => Promise<void>
|
||||
createVerificationRequest?: (
|
||||
email: string,
|
||||
url: string,
|
||||
token: string,
|
||||
secret: string,
|
||||
provider: EmailAppProvider,
|
||||
options: AppOptions
|
||||
) => Promise<TVerificationRequest>
|
||||
getVerificationRequest?: (
|
||||
email: string,
|
||||
verificationToken: string,
|
||||
secret: string,
|
||||
provider: AppProvider
|
||||
) => Promise<TVerificationRequest | null>
|
||||
deleteVerificationRequest?: (
|
||||
email: string,
|
||||
verificationToken: string,
|
||||
secret: string,
|
||||
provider: AppProvider
|
||||
) => Promise<void>
|
||||
}
|
||||
|
||||
interface Adapter<
|
||||
TUser extends User = any,
|
||||
TProfile extends Profile = any,
|
||||
TSession extends Session = any,
|
||||
TVerificationRequest extends VerificationRequest = any
|
||||
> {
|
||||
getAdapter: (
|
||||
appOptions: AppOptions
|
||||
) => Promise<AdapterInstance<TUser, TProfile, TSession, TVerificationRequest>>
|
||||
}
|
||||
|
||||
type Schema<T = any> = EntitySchema<T>["options"]
|
||||
|
||||
interface BuiltInAdapters {
|
||||
Default: TypeORMAdapter["Adapter"]
|
||||
TypeORM: TypeORMAdapter
|
||||
Prisma: PrismaAdapter
|
||||
/** Legacy */
|
||||
declare const Adapters: {
|
||||
Default: Adapter<ConnectionOptions>
|
||||
TypeORM: { Adapter: Adapter<ConnectionOptions> }
|
||||
Prisma: { Adapter: Adapter }
|
||||
}
|
||||
export default Adapters
|
||||
|
||||
/**
|
||||
* TODO: fix auto-type schema
|
||||
* Using a custom adapter you can connect to any database backend or even several different databases.
|
||||
* Custom adapters created and maintained by our community can be found in the adapters repository.
|
||||
* Feel free to add a custom adapter from your project to the repository,
|
||||
* or even become a maintainer of a certain adapter.
|
||||
* Custom adapters can still be created and used in a project without being added to the repository.
|
||||
*
|
||||
* [Community adapters](https://github.com/nextauthjs/adapters) |
|
||||
* [Create a custom adapter](https://next-auth.js.org/tutorials/creating-a-database-adapter)
|
||||
*/
|
||||
|
||||
interface TypeORMAdapter<
|
||||
A extends TypeORMAccountModel = any,
|
||||
U extends TypeORMUserModel = any,
|
||||
S extends TypeORMSessionModel = any,
|
||||
VR extends TypeORMVerificationRequestModel = any
|
||||
> {
|
||||
Adapter: (
|
||||
typeOrmConfig: ConnectionOptions,
|
||||
options?: {
|
||||
models?: {
|
||||
Account?: {
|
||||
model: A
|
||||
schema: Schema<A>
|
||||
}
|
||||
User?: {
|
||||
model: U
|
||||
schema: Schema<U>
|
||||
}
|
||||
Session?: {
|
||||
model: S
|
||||
schema: Schema<S>
|
||||
}
|
||||
VerificationRequest?: {
|
||||
model: VR
|
||||
schema: Schema<VR>
|
||||
}
|
||||
}
|
||||
}
|
||||
) => Adapter<U, Profile, S, VR>
|
||||
Models: {
|
||||
Account: {
|
||||
model: TypeORMAccountModel
|
||||
schema: Schema<TypeORMAccountModel>
|
||||
}
|
||||
User: {
|
||||
model: TypeORMUserModel
|
||||
schema: Schema<TypeORMUserModel>
|
||||
}
|
||||
Session: {
|
||||
model: TypeORMSessionModel
|
||||
schema: Schema<TypeORMSessionModel>
|
||||
}
|
||||
VerificationRequest: {
|
||||
model: TypeORMVerificationRequestModel
|
||||
schema: Schema<TypeORMVerificationRequestModel>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface PrismaAdapter {
|
||||
Adapter: (config: {
|
||||
prisma: any
|
||||
modelMapping?: {
|
||||
User: string
|
||||
Account: string
|
||||
Session: string
|
||||
VerificationRequest: string
|
||||
}
|
||||
}) => Adapter
|
||||
}
|
||||
|
||||
declare class TypeORMAccountModel {
|
||||
compoundId: string
|
||||
userId: number
|
||||
providerType: string
|
||||
providerId: string
|
||||
providerAccountId: string
|
||||
refreshToken?: string
|
||||
accessToken?: string
|
||||
accessTokenExpires?: Date
|
||||
|
||||
constructor(
|
||||
userId: number,
|
||||
export interface AdapterInstance<U = User, P = Profile, S = Session> {
|
||||
createUser(profile: P): Promise<U>
|
||||
getUser(id: string): Promise<U | null>
|
||||
getUserByEmail(email: string): Promise<U | null>
|
||||
getUserByProviderAccountId(
|
||||
providerId: string,
|
||||
providerAccountId: string
|
||||
): Promise<U | null>
|
||||
updateUser(user: U): Promise<U>
|
||||
/** @todo Implement */
|
||||
deleteUser?(userId: string): Promise<void>
|
||||
linkAccount(
|
||||
userId: string,
|
||||
providerId: string,
|
||||
providerType: string,
|
||||
providerAccountId: string,
|
||||
refreshToken?: string,
|
||||
accessToken?: string,
|
||||
accessTokenExpires?: Date
|
||||
)
|
||||
accessTokenExpires?: null
|
||||
): Promise<void>
|
||||
/** @todo Implement */
|
||||
unlinkAccount?(
|
||||
userId: string,
|
||||
providerId: string,
|
||||
providerAccountId: string
|
||||
): Promise<void>
|
||||
createSession(user: U): Promise<S>
|
||||
getSession(sessionToken: string): Promise<S | null>
|
||||
updateSession(session: S, force?: boolean): Promise<S | null>
|
||||
deleteSession(sessionToken: string): Promise<void>
|
||||
createVerificationRequest?(
|
||||
identifier: string,
|
||||
url: string,
|
||||
token: string,
|
||||
secret: string,
|
||||
provider: EmailConfig & { maxAge: number; from: string }
|
||||
): Promise<void>
|
||||
getVerificationRequest?(
|
||||
identifier: string,
|
||||
verificationToken: string,
|
||||
secret: string,
|
||||
provider: Required<EmailConfig>
|
||||
): Promise<{
|
||||
id: string
|
||||
identifier: string
|
||||
token: string
|
||||
expires: Date
|
||||
} | null>
|
||||
deleteVerificationRequest?(
|
||||
identifier: string,
|
||||
verificationToken: string,
|
||||
secret: string,
|
||||
provider: Required<EmailConfig>
|
||||
): Promise<void>
|
||||
}
|
||||
|
||||
declare class TypeORMUserModel implements User {
|
||||
name?: string
|
||||
email?: string
|
||||
image?: string
|
||||
emailVerified?: Date
|
||||
|
||||
constructor(
|
||||
name?: string,
|
||||
email?: string,
|
||||
image?: string,
|
||||
emailVerified?: Date
|
||||
)
|
||||
[x: string]: unknown
|
||||
}
|
||||
|
||||
declare class TypeORMSessionModel implements Session {
|
||||
userId: number
|
||||
expires: Date
|
||||
sessionToken: string
|
||||
accessToken: string
|
||||
|
||||
constructor(
|
||||
userId: number,
|
||||
expires: Date,
|
||||
sessionToken?: string,
|
||||
accessToken?: string
|
||||
)
|
||||
}
|
||||
|
||||
declare class TypeORMVerificationRequestModel implements VerificationRequest {
|
||||
identifier: string
|
||||
token: string
|
||||
expires: Date
|
||||
|
||||
constructor(identifier: string, token: string, expires: Date)
|
||||
}
|
||||
|
||||
declare const Adapters: BuiltInAdapters
|
||||
|
||||
export default Adapters
|
||||
|
||||
export {
|
||||
Adapter,
|
||||
BuiltInAdapters as Adapters,
|
||||
TypeORMAdapter,
|
||||
TypeORMAccountModel,
|
||||
TypeORMUserModel,
|
||||
TypeORMSessionModel,
|
||||
TypeORMVerificationRequestModel,
|
||||
PrismaAdapter,
|
||||
/**
|
||||
* From an implementation perspective, an adapter in NextAuth.js is a function
|
||||
* which returns an async `getAdapter()` method, which in turn returns a list of functions
|
||||
* used to handle operations such as creating user, linking a user
|
||||
* and an OAuth account or handling reading and writing sessions.
|
||||
*
|
||||
* It uses this approach to allow database connection logic to live in the `getAdapter()` method.
|
||||
* By calling the function just before an action needs to happen,
|
||||
* it is possible to check database connection status and handle connecting / reconnecting
|
||||
* to a database as required.
|
||||
*
|
||||
* **Required methods**
|
||||
*
|
||||
* _(These methods are required for all sign in flows)_
|
||||
* - `createUser`
|
||||
* - `getUser`
|
||||
* - `getUserByEmail`
|
||||
* - `getUserByProviderAccountId`
|
||||
* - `linkAccount`
|
||||
* - `createSession`
|
||||
* - `getSession`
|
||||
* - `updateSession`
|
||||
* - `deleteSession`
|
||||
* - `updateUser`
|
||||
*
|
||||
* _(Required to support email / passwordless sign in)_
|
||||
*
|
||||
* - `createVerificationRequest`
|
||||
* - `getVerificationRequest`
|
||||
* - `deleteVerificationRequest`
|
||||
*
|
||||
* **Unimplemented methods**
|
||||
*
|
||||
* _(These methods will be required in a future release, but are not yet invoked)_
|
||||
* - `deleteUser`
|
||||
* - `unlinkAccount`
|
||||
*
|
||||
* [Community adapters](https://github.com/nextauthjs/adapters) |
|
||||
* [Create a custom adapter](https://next-auth.js.org/tutorials/creating-a-database-adapter)
|
||||
*/
|
||||
export type Adapter<
|
||||
C = Record<string, unknown>,
|
||||
O = Record<string, unknown>,
|
||||
U = User,
|
||||
P = Profile,
|
||||
S = Session
|
||||
> = (
|
||||
config: C,
|
||||
options?: O
|
||||
) => {
|
||||
getAdapter(appOptions: AppOptions): Promise<AdapterInstance<U, P, S>>
|
||||
}
|
||||
|
||||
2
types/index.d.ts
vendored
2
types/index.d.ts
vendored
@@ -233,6 +233,8 @@ export interface LoggerInstance {
|
||||
*/
|
||||
export interface TokenSet {
|
||||
accessToken: string
|
||||
/** Kept for historical reasons, check out `expires_in` */
|
||||
accessTokenExpires: null
|
||||
idToken?: string
|
||||
refreshToken?: string
|
||||
access_token: string
|
||||
|
||||
23
types/providers.d.ts
vendored
23
types/providers.d.ts
vendored
@@ -67,6 +67,7 @@ export type OAuthProviderType =
|
||||
| "EVEOnline"
|
||||
| "Facebook"
|
||||
| "FACEIT"
|
||||
| "FortyTwo"
|
||||
| "Foursquare"
|
||||
| "FusionAuth"
|
||||
| "GitHub"
|
||||
@@ -132,19 +133,27 @@ export interface EmailConfigServerOptions {
|
||||
}
|
||||
}
|
||||
|
||||
export type SendVerificationRequest = (params: {
|
||||
identifier: string
|
||||
url: string
|
||||
baseUrl: string
|
||||
token: string
|
||||
provider: EmailConfig
|
||||
}) => Awaitable<void>
|
||||
|
||||
export interface EmailConfig extends CommonProviderOptions {
|
||||
type: "email"
|
||||
// TODO: Make use of https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html
|
||||
server: string | EmailConfigServerOptions
|
||||
/** @default "NextAuth <no-reply@example.com>" */
|
||||
from?: string
|
||||
/**
|
||||
* How long until the e-mail can be used to log the user in,
|
||||
* in seconds. Defaults to 1 day
|
||||
* @default 86400
|
||||
*/
|
||||
maxAge?: number
|
||||
sendVerificationRequest(params: {
|
||||
identifier: string
|
||||
url: string
|
||||
baseUrl: string
|
||||
token: string
|
||||
provider: EmailConfig
|
||||
}): Awaitable<void>
|
||||
sendVerificationRequest: SendVerificationRequest
|
||||
}
|
||||
|
||||
export type EmailProvider = (options: Partial<EmailConfig>) => EmailConfig
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import Providers, { AppProvider, OAuthConfig } from "next-auth/providers"
|
||||
import {
|
||||
Adapter,
|
||||
EmailAppProvider,
|
||||
Profile,
|
||||
Session,
|
||||
VerificationRequest,
|
||||
} from "next-auth/adapters"
|
||||
import Providers, {
|
||||
AppProvider,
|
||||
EmailConfig,
|
||||
OAuthConfig,
|
||||
} from "next-auth/providers"
|
||||
import { Adapter, AdapterInstance } from "next-auth/adapters"
|
||||
import NextAuth, * as NextAuthTypes from "next-auth"
|
||||
import { IncomingMessage, ServerResponse } from "http"
|
||||
import * as JWTType from "next-auth/jwt"
|
||||
@@ -54,74 +52,86 @@ const exampleUser: NextAuthTypes.User = {
|
||||
email: "",
|
||||
}
|
||||
|
||||
const exampleSession: Session = {
|
||||
const exampleSession: NextAuthTypes.Session = {
|
||||
userId: "",
|
||||
accessToken: "",
|
||||
sessionToken: "",
|
||||
expires: new Date(),
|
||||
}
|
||||
|
||||
const exampleVerificatoinRequest: VerificationRequest = {
|
||||
const exampleVerificationRequest = {
|
||||
id: "",
|
||||
identifier: "",
|
||||
token: "",
|
||||
expires: new Date(),
|
||||
}
|
||||
|
||||
const adapter: Adapter<
|
||||
NextAuthTypes.User,
|
||||
Profile,
|
||||
Session,
|
||||
VerificationRequest
|
||||
> = {
|
||||
async getAdapter(appOptions: AppOptions) {
|
||||
return {
|
||||
createUser: async (profile: Profile) => exampleUser,
|
||||
getUser: async (id: string) => exampleUser,
|
||||
getUserByEmail: async (email: string) => exampleUser,
|
||||
getUserByProviderAccountId: async (
|
||||
providerId: string,
|
||||
providerAccountId: string
|
||||
) => exampleUser,
|
||||
updateUser: async (user: NextAuthTypes.User) => exampleUser,
|
||||
linkAccount: async (
|
||||
userId: string,
|
||||
providerId: string,
|
||||
providerType: string,
|
||||
providerAccountId: string,
|
||||
refreshToken: string,
|
||||
accessToken: string,
|
||||
accessTokenExpires: number
|
||||
) => undefined,
|
||||
createSession: async (user: NextAuthTypes.User) => exampleSession,
|
||||
getSession: async (sessionToken: string) => exampleSession,
|
||||
updateSession: async (session: Session, force?: boolean) =>
|
||||
exampleSession,
|
||||
deleteSession: async (sessionToken: string) => undefined,
|
||||
createVerificationRequest: async (
|
||||
email: string,
|
||||
url: string,
|
||||
token: string,
|
||||
secret: string,
|
||||
provider: EmailAppProvider,
|
||||
options: AppOptions
|
||||
) => exampleVerificatoinRequest,
|
||||
getVerificationRequest: async (
|
||||
email: string,
|
||||
verificationToken: string,
|
||||
secret: string,
|
||||
provider: AppProvider
|
||||
) => exampleVerificatoinRequest,
|
||||
deleteVerificationRequest: async (
|
||||
email: string,
|
||||
verificationToken: string,
|
||||
secret: string,
|
||||
provider: AppProvider
|
||||
) => undefined,
|
||||
}
|
||||
},
|
||||
const adapter: Adapter = () => {
|
||||
return {
|
||||
async getAdapter(appOptions: AppOptions) {
|
||||
return {
|
||||
async createUser(profile) {
|
||||
return exampleUser
|
||||
},
|
||||
async getUser(id) {
|
||||
return exampleUser
|
||||
},
|
||||
async getUserByEmail(email) {
|
||||
return exampleUser
|
||||
},
|
||||
async getUserByProviderAccountId(providerId, providerAccountId) {
|
||||
return exampleUser
|
||||
},
|
||||
async updateUser(user) {
|
||||
return exampleUser
|
||||
},
|
||||
async linkAccount(
|
||||
userId,
|
||||
providerId,
|
||||
providerType,
|
||||
providerAccountId,
|
||||
refreshToken,
|
||||
accessToken,
|
||||
accessTokenExpires
|
||||
) {
|
||||
return undefined
|
||||
},
|
||||
async createSession(user) {
|
||||
return exampleSession
|
||||
},
|
||||
async getSession(sessionToken) {
|
||||
return exampleSession
|
||||
},
|
||||
async updateSession(session, force) {
|
||||
return exampleSession
|
||||
},
|
||||
async deleteSession(sessionToken) {
|
||||
return undefined
|
||||
},
|
||||
async createVerificationRequest(email, url, token, secret, provider) {
|
||||
return undefined
|
||||
},
|
||||
async getVerificationRequest(
|
||||
email,
|
||||
verificationToken,
|
||||
secret,
|
||||
provider
|
||||
) {
|
||||
return exampleVerificationRequest
|
||||
},
|
||||
async deleteVerificationRequest(
|
||||
email,
|
||||
verificationToken,
|
||||
secret,
|
||||
provider
|
||||
) {
|
||||
return undefined
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const allConfig = {
|
||||
const allConfig: NextAuthTypes.NextAuthOptions = {
|
||||
providers: [
|
||||
Providers.Twitter({
|
||||
clientId: "123",
|
||||
@@ -147,49 +157,36 @@ const allConfig = {
|
||||
},
|
||||
pages: pageOptions,
|
||||
callbacks: {
|
||||
async signIn(
|
||||
user: NextAuthTypes.User,
|
||||
account: Record<string, unknown>,
|
||||
profile: Record<string, unknown>
|
||||
) {
|
||||
async signIn(user, account, profile) {
|
||||
return true
|
||||
},
|
||||
async redirect(url: string, baseUrl: string) {
|
||||
async redirect(url, baseUrl) {
|
||||
return "path/to/foo"
|
||||
},
|
||||
async session(
|
||||
session: NextAuthTypes.Session,
|
||||
userOrToken: NextAuthTypes.User
|
||||
) {
|
||||
async session(session, userOrToken) {
|
||||
return { ...session }
|
||||
},
|
||||
async jwt(
|
||||
token: JWTType.JWT,
|
||||
user?: NextAuthTypes.User,
|
||||
account?: Record<string, unknown>,
|
||||
profile?: Record<string, unknown>,
|
||||
isNewUser?: boolean
|
||||
) {
|
||||
async jwt(token, user, account, profile, isNewUser) {
|
||||
return token
|
||||
},
|
||||
},
|
||||
events: {
|
||||
async signIn(message: string) {
|
||||
async signIn(message) {
|
||||
return undefined
|
||||
},
|
||||
async signOut(message: string) {
|
||||
async signOut(message) {
|
||||
return undefined
|
||||
},
|
||||
async createUser(message: string) {
|
||||
async createUser(message) {
|
||||
return undefined
|
||||
},
|
||||
async linkAccount(message: string) {
|
||||
async linkAccount(message) {
|
||||
return undefined
|
||||
},
|
||||
async session(message: string) {
|
||||
async session(message) {
|
||||
return undefined
|
||||
},
|
||||
async error(message: string) {
|
||||
async error(message) {
|
||||
return undefined
|
||||
},
|
||||
},
|
||||
|
||||
@@ -76,7 +76,7 @@ As an example of what this looks like, this is the provider object returned for
|
||||
profileUrl: "https://www.googleapis.com/oauth2/v1/userinfo?alt=json",
|
||||
async profile(profile, tokens) {
|
||||
// You can use the tokens, in case you want to fetch more profile information
|
||||
// For example several OAuth provider does not return e-mail by default.
|
||||
// For example several OAuth providers do not return email by default.
|
||||
// Depending on your provider, will have tokens like `access_token`, `id_token` and or `refresh_token`
|
||||
return {
|
||||
id: profile.id,
|
||||
@@ -131,7 +131,7 @@ providers: [
|
||||
| requestTokenUrl | Endpoint to retrieve a request token | `string` | No |
|
||||
| authorizationParams | Additional params to be sent to the authorization endpoint | `object` | No |
|
||||
| profileUrl | Endpoint to retrieve the user's profile | `string` | No |
|
||||
| profile | An callback returning an object with the user's info | `object` | No |
|
||||
| profile | A callback returning an object with the user's info | `object` | No |
|
||||
| idToken | Set to `true` for services that use ID Tokens (e.g. OpenID) | `boolean` | No |
|
||||
| headers | Any headers that should be sent to the OAuth provider | `object` | No |
|
||||
| protection | Additional security for OAuth login flows (defaults to `state`) |`[pkce]`,`[state]`,`[pkce,state]`| No |
|
||||
@@ -233,4 +233,4 @@ If you think your custom provider might be useful to others, we encourage you to
|
||||
|
||||
That's it! 🎉 Others will be able to discover this provider much more easily now!
|
||||
|
||||
You can look at the existing built-in providers for inspiration.
|
||||
You can look at the existing built-in providers for inspiration.
|
||||
|
||||
26
www/docs/providers/42.md
Normal file
26
www/docs/providers/42.md
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
id: 42-school
|
||||
title: 42 School
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
https://api.intra.42.fr/apidoc/guides/web_application_flow
|
||||
|
||||
## Configuration
|
||||
|
||||
https://profile.intra.42.fr/oauth/applications/new
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
...
|
||||
providers: [
|
||||
Providers.FortyTwo({
|
||||
clientId: process.env.FORTY_TWO_CLIENT_ID,
|
||||
clientSecret: process.env.FORTY_TWO_CLIENT_SECRET
|
||||
})
|
||||
]
|
||||
...
|
||||
```
|
||||
@@ -40,141 +40,95 @@ These methods are required to support email / passwordless sign in:
|
||||
|
||||
These methods will be required in a future release, but are not yet invoked:
|
||||
|
||||
* getUserByCredentials
|
||||
* deleteUser
|
||||
* unlinkAccount
|
||||
|
||||
### Example code
|
||||
|
||||
```js
|
||||
const Adapter = (config, options = {}) => {
|
||||
|
||||
async function getAdapter (appOptions) {
|
||||
|
||||
async function createUser (profile) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUser (id) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUserByEmail (email) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUserByProviderAccountId (
|
||||
providerId,
|
||||
providerAccountId
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUserByCredentials (credentials) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function updateUser (user) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function deleteUser (userId) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function linkAccount (
|
||||
userId,
|
||||
providerId,
|
||||
providerType,
|
||||
providerAccountId,
|
||||
refreshToken,
|
||||
accessToken,
|
||||
accessTokenExpires
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function unlinkAccount (
|
||||
userId,
|
||||
providerId,
|
||||
providerAccountId
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function createSession (user) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function getSession (sessionToken) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function updateSession (
|
||||
session,
|
||||
force
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function deleteSession (sessionToken) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function createVerificationRequest (
|
||||
identifier,
|
||||
url,
|
||||
token,
|
||||
secret,
|
||||
provider
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function getVerificationRequest (
|
||||
identifier,
|
||||
token,
|
||||
secret,
|
||||
provider
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function deleteVerificationRequest (
|
||||
identifier,
|
||||
token,
|
||||
secret,
|
||||
provider
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
createUser,
|
||||
getUser,
|
||||
getUserByEmail,
|
||||
getUserByProviderAccountId,
|
||||
getUserByCredentials,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
linkAccount,
|
||||
unlinkAccount,
|
||||
createSession,
|
||||
getSession,
|
||||
updateSession,
|
||||
deleteSession,
|
||||
createVerificationRequest,
|
||||
getVerificationRequest,
|
||||
deleteVerificationRequest
|
||||
}
|
||||
}
|
||||
|
||||
export default function YourAdapter (config, options = {}) {
|
||||
return {
|
||||
getAdapter
|
||||
async getAdapter (appOptions) {
|
||||
async createUser (profile) {
|
||||
return null
|
||||
},
|
||||
async getUser (id) {
|
||||
return null
|
||||
},
|
||||
async getUserByEmail (email) {
|
||||
return null
|
||||
},
|
||||
async getUserByProviderAccountId (
|
||||
providerId,
|
||||
providerAccountId
|
||||
) {
|
||||
return null
|
||||
},
|
||||
async updateUser (user) {
|
||||
return null
|
||||
},
|
||||
async deleteUser (userId) {
|
||||
return null
|
||||
},
|
||||
async linkAccount (
|
||||
userId,
|
||||
providerId,
|
||||
providerType,
|
||||
providerAccountId,
|
||||
refreshToken,
|
||||
accessToken,
|
||||
accessTokenExpires
|
||||
) {
|
||||
return null
|
||||
},
|
||||
async unlinkAccount (
|
||||
userId,
|
||||
providerId,
|
||||
providerAccountId
|
||||
) {
|
||||
return null
|
||||
},
|
||||
async createSession (user) {
|
||||
return null
|
||||
},
|
||||
async getSession (sessionToken) {
|
||||
return null
|
||||
},
|
||||
async updateSession (
|
||||
session,
|
||||
force
|
||||
) {
|
||||
return null
|
||||
},
|
||||
async deleteSession (sessionToken) {
|
||||
return null
|
||||
},
|
||||
async createVerificationRequest (
|
||||
identifier,
|
||||
url,
|
||||
token,
|
||||
secret,
|
||||
provider
|
||||
) {
|
||||
return null
|
||||
},
|
||||
async getVerificationRequest (
|
||||
identifier,
|
||||
token,
|
||||
secret,
|
||||
provider
|
||||
) {
|
||||
return null
|
||||
},
|
||||
async deleteVerificationRequest (
|
||||
identifier,
|
||||
token,
|
||||
secret,
|
||||
provider
|
||||
) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
Adapter
|
||||
}
|
||||
```
|
||||
|
||||
@@ -89,6 +89,12 @@ a:hover,
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 996px) {
|
||||
.main-wrapper > div {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.docusaurus-highlight-code-line {
|
||||
background-color: rgb(72, 77, 91);
|
||||
display: block;
|
||||
|
||||
Reference in New Issue
Block a user